tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is a python API to work with some methods of Tinkoff Open API using REST protocol. It can view history, orders and market information. Also, you can open orders and trades.
If you run this module as CLI program then it realizes simple logic: receiving a lot of options and execute one command. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is a python API to work with some methods of Tinkoff Open API using REST protocol. 6It can view history, orders and market information. Also, you can open orders and trades. 7 8If you run this module as CLI program then it realizes simple logic: receiving a lot of options and execute one command. 9**See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 10 11**Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 12 13About Tinkoff Invest API: https://tinkoff.github.io/investAPI/ 14 15Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/ 16""" 17 18# Copyright (c) 2022 Gilmillin Timur Mansurovich 19# 20# Licensed under the Apache License, Version 2.0 (the "License"); 21# you may not use this file except in compliance with the License. 22# You may obtain a copy of the License at 23# 24# http://www.apache.org/licenses/LICENSE-2.0 25# 26# Unless required by applicable law or agreed to in writing, software 27# distributed under the License is distributed on an "AS IS" BASIS, 28# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29# See the License for the specific language governing permissions and 30# limitations under the License. 31 32 33import sys 34import os 35from argparse import ArgumentParser 36from importlib.metadata import version 37 38from datetime import datetime, timedelta 39from dateutil.tz import tzlocal, tzutc 40from time import sleep 41 42import re 43import json 44import requests 45import traceback as tb 46from typing import Union 47 48from multiprocessing import cpu_count 49from multiprocessing.pool import ThreadPool 50import pandas as pd 51 52from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 53 54from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator 55from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 56 57import UniLogger as uLog # Logger for TKSBrokerAPI 58 59 60# --- Common technical parameters: 61 62PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 63uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 64uLogger.level = 10 # debug level by default for TKSBrokerAPI module 65uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 66 67__version__ = "1.4" # The "major.minor" version setup here, but build number define at the build-server only 68 69CPU_COUNT = cpu_count() # host's real CPU count 70CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 71 72# --- Main constants: 73 74NANO = 0.000000001 # SI-constant nano = 10^-9 75 76 77def NanoToFloat(units: str, nano: int) -> float: 78 """ 79 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 80 81 `NanoToFloat(units="2", nano=500000000) -> 2.5` 82 83 `NanoToFloat(units="0", nano=50000000) -> 0.05` 84 85 :param units: integer string or integer parameter that represents the integer part of number 86 :param nano: integer string or integer parameter that represents the fractional part of number 87 :return: float view of number 88 """ 89 return int(units) + int(nano) * NANO 90 91 92def FloatToNano(number: float) -> dict: 93 """ 94 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 95 96 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 97 98 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 99 100 :param number: float number 101 :return: nano-type view of number: `{"units": "string", "nano": integer}` 102 """ 103 splitByPoint = str(number).split(".") 104 frac = 0 105 106 if len(splitByPoint) > 1: 107 if len(splitByPoint[1]) <= 9: 108 frac = int("{}{}".format( 109 int(splitByPoint[1]), 110 "0" * (9 - len(splitByPoint[1])), 111 )) 112 113 if (number < 0) and (frac > 0): 114 frac = -frac 115 116 return {"units": str(int(number)), "nano": frac} 117 118 119def GetDatesAsString(start: str = None, end: str = None) -> tuple: 120 """ 121 Create tuple of date and time strings with timezone parsed from user-friendly date. 122 123 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 124 125 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 126 An error exception will occur if input date has incorrect format. 127 128 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 129 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 130 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 131 Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago. 132 133 Also, you can use keywords for start if `end=None`: 134 `today` (from 00:00:00 to the end of current day), 135 `yesterday` (-1 day from 00:00:00 to 23:59:59), 136 `week` (-7 day from 00:00:00 to the end of current day), 137 `month` (-30 day from 00:00:00 to the end of current day), 138 `year` (-365 day from 00:00:00 to the end of current day), 139 140 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 141 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 142 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 143 """ 144 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 145 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 146 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 147 148 # time between start and the end of the current day: 149 if start is None or start.lower() == "today": 150 pass 151 152 # from start of the last day to the end of the last day: 153 elif start.lower() == "yesterday": 154 s -= timedelta(days=1) 155 e -= timedelta(days=1) 156 157 # week (-7 day from 00:00:00 to the end of the current day): 158 elif start.lower() == "week": 159 s -= timedelta(days=6) # +1 current day already taken into account 160 161 # month (-30 day from 00:00:00 to the end of current day): 162 elif start.lower() == "month": 163 s -= timedelta(days=29) # +1 current day already taken into account 164 165 # year (-365 day from 00:00:00 to the end of current day): 166 elif start.lower() == "year": 167 s -= timedelta(days=364) # +1 current day already taken into account 168 169 # -N days ago to the end of current day: 170 elif start.startswith('-') and start[1:].isdigit(): 171 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 172 173 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 174 else: 175 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 176 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 177 178 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 179 s = s.strftime(TKS_DATE_TIME_FORMAT) 180 e = e.strftime(TKS_DATE_TIME_FORMAT) 181 182 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 183 184 return s, e 185 186 187class TinkoffBrokerServer: 188 """ 189 This class implements methods to work with Tinkoff broker server. 190 191 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 192 193 About `token`: https://tinkoff.github.io/investAPI/token/ 194 """ 195 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 196 """ 197 Main class init. 198 199 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 200 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 201 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 202 :param useCache: use default cache file with raw data to use instead of `iList`. 203 True by default. Cache is auto-update if new day has come. 204 If you don't want to use cache and always updates raw data then set `useCache=False`. 205 :param defaultCache: path to default cache file. `dump.json` by default. 206 """ 207 if token is None or not token: 208 try: 209 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 210 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 211 212 except KeyError: 213 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 214 raise Exception("Token required") 215 216 else: 217 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 218 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 219 220 if accountId is None or not accountId: 221 try: 222 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 223 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 224 225 except KeyError: 226 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 227 228 else: 229 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 230 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 231 232 self.version = __version__ # duplicate here used TKSBrokerAPI main version 233 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 234 235 Latest version: https://pypi.org/project/tksbrokerapi/ 236 """ 237 238 self.aliases = TKS_TICKER_ALIASES 239 """Some aliases instead official tickers. 240 241 See also: `TKSEnums.TKS_TICKER_ALIASES` 242 """ 243 244 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 245 246 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 247 248 self.ticker = "" 249 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 250 251 See also: `SearchByTicker()`, `SearchInstruments()`. 252 """ 253 254 self.figi = "" 255 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 256 257 See also: `SearchByFIGI()`, `SearchInstruments()`. 258 """ 259 260 self.depth = 1 261 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 262 263 See also: `GetCurrentPrices()`. 264 """ 265 266 self.server = r"https://invest-public-api.tinkoff.ru/rest" 267 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 268 269 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 270 """ 271 272 uLogger.debug("Broker API server: {}".format(self.server)) 273 274 self.timeout = 15 275 """Server operations timeout in seconds. Default: `15`. 276 277 See also: `SendAPIRequest()`. 278 """ 279 280 self.headers = { 281 "Content-Type": "application/json", 282 "accept": "application/json", 283 "Authorization": "Bearer {}".format(self.token), 284 "x-app-name": "Tim55667757.TKSBrokerAPI", 285 } 286 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 287 288 See also: `SendAPIRequest()`. 289 """ 290 291 self.body = None 292 """Request body which send to broker server. Default: `None`. 293 294 See also: `SendAPIRequest()`. 295 """ 296 297 self.historyFile = None 298 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only pandas dataframe. 299 300 See also: `History()`. 301 """ 302 303 self.htmlHistoryFile = "index.html" 304 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 305 306 See also: `ShowHistoryChart()`. 307 """ 308 309 self.instrumentsFile = "instruments.md" 310 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 311 312 See also: `ShowInstrumentsInfo()`. 313 """ 314 315 self.searchResultsFile = "search-results.md" 316 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 317 318 See also: `SearchInstruments()`. 319 """ 320 321 self.pricesFile = "prices.md" 322 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 323 324 See also: `GetListOfPrices()`. 325 """ 326 327 self.infoFile = "info.md" 328 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 329 330 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 331 """ 332 333 self.bondsXLSXFile = "ext-bonds.xlsx" 334 """Filename where wider pandas dataframe with more information about bonds: main info, current prices, 335 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 336 337 See also: `ExtendBondsData()`. 338 """ 339 340 self.calendarFile = "calendar.md" 341 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 342 343 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 344 345 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 346 """ 347 348 self.overviewFile = "overview.md" 349 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 350 351 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 352 """ 353 354 self.overviewDigestFile = "overview-digest.md" 355 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 356 357 See also: `Overview()` with parameter `details="digest"`. 358 """ 359 360 self.overviewPositionsFile = "overview-positions.md" 361 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 362 363 See also: `Overview()` with parameter `details="positions"`. 364 """ 365 366 self.overviewOrdersFile = "overview-orders.md" 367 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 368 369 See also: `Overview()` with parameter `details="orders"`. 370 """ 371 372 self.overviewAnalyticsFile = "overview-analytics.md" 373 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 374 375 See also: `Overview()` with parameter `details="analytics"`. 376 """ 377 378 self.reportFile = "deals.md" 379 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 380 381 See also: `Deals()`. 382 """ 383 384 self.withdrawalLimitsFile = "limits.md" 385 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 386 387 See also: `OverviewLimits()` and `RequestLimits()`. 388 """ 389 390 self.userInfoFile = "user-info.md" 391 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 392 393 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 394 """ 395 396 self.userAccountsFile = "accounts.md" 397 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 398 399 See also: `OverviewAccounts()`, `RequestAccounts()`. 400 """ 401 402 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 403 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 404 405 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 406 407 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 408 """ 409 410 self.iList = None # init iList for raw instruments data 411 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 412 413 See also: `Listing()`, `DumpInstruments()`. 414 """ 415 416 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 417 if useCache: 418 if os.path.exists(self.iListDumpFile): 419 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 420 curTime = datetime.now(tzutc()) 421 422 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 423 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 424 425 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 426 427 else: 428 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 429 430 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 431 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 432 433 else: 434 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 435 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 436 437 else: 438 self.iList = self.Listing() # request new raw instruments data from broker server 439 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 440 441 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 442 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 443 444 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 445 """ 446 447 @staticmethod 448 def _ParseJSON(rawData="{}", debug: bool = False) -> dict: 449 """ 450 Parse JSON from response string. 451 452 :param rawData: this is a string with JSON-formatted text. 453 :param debug: if `True` then print more debug information. 454 :return: JSON (dictionary), parsed from server response string. 455 """ 456 if debug: 457 uLogger.debug("Raw text body:") 458 uLogger.debug(rawData) 459 460 responseJSON = json.loads(rawData) if rawData else {} 461 462 if debug: 463 uLogger.debug("JSON formatted:") 464 for jsonLine in json.dumps(responseJSON, indent=4).split('\n'): 465 uLogger.debug(jsonLine) 466 467 return responseJSON 468 469 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 470 """ 471 Send GET or POST request to broker server and receive JSON object. 472 473 self.header: must be defining with dictionary of headers. 474 self.body: if define then used as request body. None by default. 475 self.timeout: global request timeout, 15 seconds by default. 476 :param url: url with REST request. 477 :param reqType: send "GET" or "POST" request. "GET" by default. 478 :param retry: how many times retry after first request if an 5xx server errors occurred. 479 :param pause: sleep time in seconds between retries. 480 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 481 :return: response JSON (dictionary) from broker. 482 """ 483 if reqType not in ("GET", "POST"): 484 uLogger.error("You can define request type: 'GET' or 'POST'!") 485 raise Exception("Incorrect value") 486 487 if debug: 488 uLogger.debug("Request parameters:") 489 uLogger.debug(" - REST API URL: {}".format(url)) 490 uLogger.debug(" - request type: {}".format(reqType)) 491 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 492 uLogger.debug(" - body: {}".format(self.body)) 493 494 # fast hack to avoid all operations with some tickers/FIGI 495 responseJSON = {} 496 oK = True 497 for item in self.exclude: 498 if item in url: 499 if debug: 500 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 501 502 oK = False 503 break 504 505 if oK: 506 counter = 0 507 response = None 508 errMsg = "" 509 510 while not response and counter <= retry: 511 if reqType == "GET": 512 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 513 514 if reqType == "POST": 515 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 516 517 if debug: 518 uLogger.debug("Response:") 519 uLogger.debug(" - status code: {}".format(response.status_code)) 520 uLogger.debug(" - reason: {}".format(response.reason)) 521 uLogger.debug(" - body length: {}".format(len(response.text))) 522 uLogger.debug(" - headers: {}".format(response.headers)) 523 524 # Server returns some headers: 525 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 526 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 527 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 528 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 529 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 530 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 531 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 532 sleep(rateLimitWait) 533 534 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 535 if 400 <= response.status_code < 500: 536 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 537 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 538 counter = retry + 1 539 540 if 500 <= response.status_code < 600: 541 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 542 uLogger.debug(" - not oK, {}".format(errMsg)) 543 counter += 1 544 545 if counter <= retry: 546 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 547 sleep(pause) 548 549 responseJSON = self._ParseJSON(response.text) 550 551 if errMsg: 552 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 553 uLogger.error(" - not oK, {}".format(errMsg)) 554 555 return responseJSON 556 557 def _IUpdater(self, iType: str) -> tuple: 558 """ 559 Request instrument by type from server. See available API methods for instruments: 560 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 561 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 562 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 563 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 564 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 565 566 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 567 :return: tuple with iType name and list of available instruments of current type for defined user token. 568 """ 569 result = [] 570 571 if iType in TKS_INSTRUMENTS: 572 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 573 574 # all instruments have the same body in API v2 requests: 575 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 576 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 577 result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"] 578 579 return iType, result 580 581 def _IWrapper(self, kwargs): 582 """ 583 Wrapper runs instrument's update method `_IUpdater()`. 584 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 585 """ 586 return self._IUpdater(**kwargs) 587 588 def Listing(self) -> dict: 589 """ 590 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 591 592 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 593 """ 594 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 595 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 596 597 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 598 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 599 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 600 601 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 602 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 603 poolUpdater.close() 604 605 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 606 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 607 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 608 609 # calculate minimum price increment (step) for all instruments and set up instrument's type: 610 for iType in iList.keys(): 611 for ticker in iList[iType]: 612 iList[iType][ticker]["type"] = iType 613 614 if "minPriceIncrement" in iList[iType][ticker].keys(): 615 iList[iType][ticker]["step"] = NanoToFloat( 616 iList[iType][ticker]["minPriceIncrement"]["units"], 617 iList[iType][ticker]["minPriceIncrement"]["nano"], 618 ) 619 620 else: 621 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 622 623 return iList 624 625 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 626 """ 627 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 628 629 See also: `DumpInstruments()`, `Listing()`. 630 631 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 632 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 633 """ 634 if self.iListDumpFile is None or not self.iListDumpFile: 635 uLogger.error("Output name of dump file must be defined!") 636 raise Exception("Filename required") 637 638 if not self.iList or forceUpdate: 639 self.iList = self.Listing() 640 641 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 642 643 # Save as XLSX with separated sheets for every type of instruments: 644 with pd.ExcelWriter( 645 path=xlsxDumpFile, 646 date_format=TKS_DATE_FORMAT, 647 datetime_format=TKS_DATE_TIME_FORMAT, 648 mode="w", 649 ) as writer: 650 for iType in TKS_INSTRUMENTS: 651 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 652 df = df[sorted(df)] # sorted by column names 653 df = df.applymap( 654 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 655 na_action="ignore", 656 ) # converting numbers from nano-type to float in every cell 657 df.to_excel( 658 writer, 659 sheet_name=iType, 660 encoding="UTF-8", 661 freeze_panes=(1, 1), 662 ) # saving as XLSX-file with freeze first row and column as headers 663 664 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 665 666 def DumpInstruments(self, forceUpdate: bool = True) -> str: 667 """ 668 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 669 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 670 671 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 672 673 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 674 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 675 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 676 """ 677 if self.iListDumpFile is None or not self.iListDumpFile: 678 uLogger.error("Output name of dump file must be defined!") 679 raise Exception("Filename required") 680 681 if not self.iList or forceUpdate: 682 self.iList = self.Listing() 683 684 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 685 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 686 fH.write(jsonDump) 687 688 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 689 690 return jsonDump 691 692 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 693 """ 694 Show information about one instrument defined by json data and prints it in Markdown format. 695 696 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 697 698 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 699 :param show: if `True` then also printing information about instrument and its current price. 700 :return: multilines text in Markdown format with information about one instrument. 701 """ 702 splitLine = "| | |\n" 703 infoText = "" 704 705 if iJSON is not None and iJSON and isinstance(iJSON, dict): 706 info = [ 707 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 708 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 709 "| Parameters | Values |\n", 710 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 711 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 712 "| Full name: | {:<54} |\n".format(iJSON["name"]), 713 ] 714 715 if "sector" in iJSON.keys() and iJSON["sector"]: 716 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 717 718 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 719 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 720 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 721 ))) 722 723 info.extend([ 724 splitLine, 725 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 726 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 727 ]) 728 729 if "isin" in iJSON.keys() and iJSON["isin"]: 730 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 731 732 if "classCode" in iJSON.keys(): 733 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 734 735 info.extend([ 736 splitLine, 737 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 738 splitLine, 739 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 740 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 741 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 742 ]) 743 744 if iJSON["figi"]: 745 self.figi = iJSON["figi"] 746 iJSON = iJSON | self.RequestTradingStatus() 747 748 info.extend([ 749 splitLine, 750 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 751 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 752 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 753 ]) 754 755 info.append(splitLine) 756 757 if "type" in iJSON.keys() and iJSON["type"]: 758 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 759 760 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 761 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 762 763 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 764 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 765 766 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 767 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 768 769 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 770 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 771 772 if "focusType" in iJSON.keys() and iJSON["focusType"]: 773 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 774 775 if "assetType" in iJSON.keys() and iJSON["assetType"]: 776 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 777 778 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 779 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 780 781 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 782 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 783 784 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 785 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 786 787 if "currency" in iJSON.keys(): 788 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 789 790 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 791 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 792 793 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 794 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 795 796 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 797 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 798 799 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 800 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 801 802 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 803 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 804 805 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 806 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 807 808 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 809 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 810 811 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 812 info.append("| Perpetual bond: | Yes |\n") 813 814 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 815 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 816 817 iExt = None 818 if iJSON["type"] == "Bonds": 819 info.extend([ 820 splitLine, 821 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 822 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 823 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 824 iJSON["nominal"]["currency"], 825 )), 826 ]) 827 828 if "floatingCouponFlag" in iJSON.keys(): 829 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 830 831 if "amortizationFlag" in iJSON.keys(): 832 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 833 834 info.append(splitLine) 835 836 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 837 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 838 839 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 840 841 info.extend([ 842 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 843 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 844 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 845 ]) 846 847 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 848 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 849 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 850 iJSON["aciValue"]["currency"] 851 ))) 852 853 if "currentPrice" in iJSON.keys(): 854 info.append(splitLine) 855 856 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 857 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 858 859 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 860 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 861 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 862 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 863 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 864 865 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 866 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 867 868 info.extend([ 869 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 870 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 871 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 872 )), 873 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 874 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 875 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 876 )), 877 "| Changes between last deal price and last close | {:<54} |\n".format( 878 "{:.2f}%{}".format( 879 iJSON["currentPrice"]["changes"], 880 " ({}{:.2f} {})".format( 881 "+" if bondChangesDelta > 0 else "", 882 bondChangesDelta, 883 aciCurrency 884 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 885 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 886 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 887 currency 888 ), 889 ) 890 ), 891 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 892 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 893 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 894 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 895 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 896 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 897 )), 898 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 899 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 900 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 901 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 902 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 903 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 904 )), 905 ]) 906 907 if "lot" in iJSON.keys(): 908 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 909 910 if "step" in iJSON.keys() and iJSON["step"] != 0: 911 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 912 913 # Add bond payment calendar: 914 if iJSON["type"] == "Bonds": 915 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 916 info.extend(["\n", strCalendar]) 917 918 infoText += "".join(info) 919 920 if show: 921 uLogger.info("{}".format(infoText)) 922 923 else: 924 uLogger.debug("{}".format(infoText)) 925 926 if self.infoFile is not None: 927 with open(self.infoFile, "w", encoding="UTF-8") as fH: 928 fH.write(infoText) 929 930 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 931 932 return infoText 933 934 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 935 """ 936 Search and return raw broker's information about instrument by its ticker. 937 `ticker` must be defined! If debug=True then print all debug messages. 938 939 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 940 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 941 :param debug: if `True` then print all debug console messages. 942 :return: JSON formatted data with information about instrument. 943 """ 944 tickerJSON = {} 945 if debug: 946 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 947 948 if not self.ticker: 949 uLogger.warning("self.ticker variable is not be empty!") 950 951 else: 952 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 953 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 954 raise Exception("Instrument not allowed") 955 956 if not self.iList: 957 self.iList = self.Listing() 958 959 if self.ticker in self.iList["Shares"].keys(): 960 tickerJSON = self.iList["Shares"][self.ticker] 961 if debug: 962 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 963 964 elif self.ticker in self.iList["Currencies"].keys(): 965 tickerJSON = self.iList["Currencies"][self.ticker] 966 if debug: 967 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 968 969 elif self.ticker in self.iList["Bonds"].keys(): 970 tickerJSON = self.iList["Bonds"][self.ticker] 971 if debug: 972 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 973 974 elif self.ticker in self.iList["Etfs"].keys(): 975 tickerJSON = self.iList["Etfs"][self.ticker] 976 if debug: 977 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 978 979 elif self.ticker in self.iList["Futures"].keys(): 980 tickerJSON = self.iList["Futures"][self.ticker] 981 if debug: 982 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 983 984 if tickerJSON: 985 self.figi = tickerJSON["figi"] 986 987 if requestPrice: 988 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 989 990 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 991 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 992 993 else: 994 tickerJSON["currentPrice"]["changes"] = 0 995 996 if show: 997 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 998 999 else: 1000 if show: 1001 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1002 1003 return tickerJSON 1004 1005 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1006 """ 1007 Search and return raw broker's information about instrument by its FIGI. 1008 `figi` must be defined! If debug=True then print all debug messages. 1009 1010 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1011 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1012 :param debug: if `True` then print all debug console messages. 1013 :return: JSON formatted data with information about instrument. 1014 """ 1015 figiJSON = {} 1016 if debug: 1017 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1018 1019 if not self.figi: 1020 uLogger.warning("self.figi variable is not be empty!") 1021 1022 else: 1023 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1024 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1025 raise Exception("Instrument not allowed") 1026 1027 if not self.iList: 1028 self.iList = self.Listing() 1029 1030 for item in self.iList["Shares"].keys(): 1031 if self.figi == self.iList["Shares"][item]["figi"]: 1032 figiJSON = self.iList["Shares"][item] 1033 1034 if debug: 1035 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1036 1037 break 1038 1039 if not figiJSON: 1040 for item in self.iList["Currencies"].keys(): 1041 if self.figi == self.iList["Currencies"][item]["figi"]: 1042 figiJSON = self.iList["Currencies"][item] 1043 1044 if debug: 1045 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1046 1047 break 1048 1049 if not figiJSON: 1050 for item in self.iList["Bonds"].keys(): 1051 if self.figi == self.iList["Bonds"][item]["figi"]: 1052 figiJSON = self.iList["Bonds"][item] 1053 1054 if debug: 1055 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1056 1057 break 1058 1059 if not figiJSON: 1060 for item in self.iList["Etfs"].keys(): 1061 if self.figi == self.iList["Etfs"][item]["figi"]: 1062 figiJSON = self.iList["Etfs"][item] 1063 1064 if debug: 1065 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1066 1067 break 1068 1069 if not figiJSON: 1070 for item in self.iList["Futures"].keys(): 1071 if self.figi == self.iList["Futures"][item]["figi"]: 1072 figiJSON = self.iList["Futures"][item] 1073 1074 if debug: 1075 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1076 1077 break 1078 1079 if figiJSON: 1080 self.figi = figiJSON["figi"] 1081 self.ticker = figiJSON["ticker"] 1082 1083 if requestPrice: 1084 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1085 1086 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1087 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1088 1089 else: 1090 figiJSON["currentPrice"]["changes"] = 0 1091 1092 if show: 1093 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1094 1095 else: 1096 if show: 1097 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1098 1099 return figiJSON 1100 1101 def GetCurrentPrices(self, show: bool = True) -> dict: 1102 """ 1103 Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record: 1104 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1105 1106 See also: 1107 1108 :param show: if `True` then print DOM to log and console. 1109 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1110 """ 1111 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1112 1113 if self.depth < 1: 1114 uLogger.error("Depth of Market (DOM) must be >=1!") 1115 raise Exception("Incorrect value") 1116 1117 if not (self.ticker or self.figi): 1118 uLogger.error("self.ticker or self.figi variables must be defined!") 1119 raise Exception("Ticker or FIGI required") 1120 1121 if self.ticker and not self.figi: 1122 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1123 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1124 1125 if not self.ticker and self.figi: 1126 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1127 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1128 1129 if not self.figi: 1130 uLogger.error("FIGI is not defined!") 1131 raise Exception("Ticker or FIGI required") 1132 1133 else: 1134 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1135 1136 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1137 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1138 self.body = str({"figi": self.figi, "depth": self.depth}) 1139 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") 1140 1141 if pricesResponse: 1142 # list of dicts with sellers orders: 1143 prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1144 1145 # list of dicts with buyers orders: 1146 prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1147 1148 # max price of instrument at this time: 1149 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1150 1151 # min price of instrument at this time: 1152 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1153 1154 # last price of deal with instrument: 1155 prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0 1156 1157 # last close price of instrument: 1158 prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0 1159 1160 else: 1161 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1162 uLogger.debug("Server response: {}".format(pricesResponse)) 1163 1164 if show: 1165 if prices["buy"] or prices["sell"]: 1166 info = [ 1167 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1168 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1169 self.ticker, 1170 self.figi, 1171 self.depth, 1172 ), 1173 uLog.sepShort, "\n", 1174 " Orders of Buyers | Orders of Sellers\n", 1175 uLog.sepShort, "\n", 1176 " Sell prices (vol.) | Buy prices (vol.)\n", 1177 uLog.sepShort, "\n", 1178 ] 1179 1180 if not prices["buy"]: 1181 info.append(" | No orders!\n") 1182 sumBuy = 0 1183 1184 else: 1185 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1186 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1187 for item in maxMinSorted: 1188 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1189 1190 if not prices["sell"]: 1191 info.append("No orders! |\n") 1192 sumSell = 0 1193 1194 else: 1195 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1196 for item in prices["sell"]: 1197 info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1198 1199 info.extend([ 1200 uLog.sepShort, "\n", 1201 "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1202 uLog.sepShort, "\n", 1203 ]) 1204 1205 infoText = "".join(info) 1206 1207 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1208 1209 else: 1210 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1211 1212 return prices 1213 1214 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1215 """ 1216 This method get and show information about all available broker instruments for current user account. 1217 If `instrumentsFile` string is not empty then also save information to this file. 1218 1219 :param show: if `True` then print results to console, if `False` - print only to file. 1220 :return: multi-lines string with all available broker instruments 1221 """ 1222 if not self.iList: 1223 self.iList = self.Listing() 1224 1225 info = [ 1226 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1227 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1228 ] 1229 1230 # add instruments count by type: 1231 for iType in self.iList.keys(): 1232 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1233 1234 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1235 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1236 1237 # generating info tables with all instruments by type: 1238 for iType in self.iList.keys(): 1239 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1240 1241 for instrument in self.iList[iType].keys(): 1242 iName = self.iList[iType][instrument]["name"] # instrument's name 1243 if len(iName) > 57: 1244 iName = "{}...".format(iName[:54]) # right trim for a long string 1245 1246 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1247 self.iList[iType][instrument]["ticker"], 1248 iName, 1249 self.iList[iType][instrument]["figi"], 1250 self.iList[iType][instrument]["currency"], 1251 self.iList[iType][instrument]["lot"], 1252 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1253 )) 1254 1255 infoText = "".join(info) 1256 1257 if show: 1258 uLogger.info(infoText) 1259 1260 if self.instrumentsFile: 1261 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1262 fH.write(infoText) 1263 1264 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1265 1266 return infoText 1267 1268 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1269 """ 1270 This method search and show information about instruments by part of its ticker, FIGI or name. 1271 If `searchResultsFile` string is not empty then also save information to this file. 1272 1273 :param pattern: string with part of ticker, FIGI or instrument's name. 1274 :param show: if `True` then print results to console, if `False` - return list of result only. 1275 :return: list of dictionaries with all found instruments. 1276 """ 1277 if not self.iList: 1278 self.iList = self.Listing() 1279 1280 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1281 compiledPattern = re.compile(pattern, re.IGNORECASE) 1282 1283 for iType in self.iList: 1284 for instrument in self.iList[iType].values(): 1285 searchResult = compiledPattern.search(" ".join( 1286 [instrument["ticker"], instrument["figi"], instrument["name"]] 1287 )) 1288 1289 if searchResult: 1290 searchResults[iType][instrument["ticker"]] = instrument 1291 1292 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1293 info = [ 1294 "# Search results\n\n", 1295 "* **Search pattern:** [{}]\n".format(pattern), 1296 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1297 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1298 ] 1299 infoShort = info[:] 1300 1301 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1302 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1303 skippedLine = "| ... | ... | ... | ... |\n" 1304 1305 if resultsLen == 0: 1306 info.append("\nNo results\n") 1307 infoShort.append("\nNo results\n") 1308 uLogger.warning("No results. Try changing your search pattern.") 1309 1310 else: 1311 for iType in searchResults: 1312 iTypeValuesCount = len(searchResults[iType].values()) 1313 if iTypeValuesCount > 0: 1314 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1315 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1316 1317 for instrument in searchResults[iType].values(): 1318 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1319 instrument["type"], 1320 instrument["ticker"], 1321 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1322 instrument["figi"], 1323 )) 1324 1325 if iTypeValuesCount <= 5: 1326 infoShort.extend(info[-iTypeValuesCount:]) 1327 1328 else: 1329 infoShort.extend(info[-5:]) 1330 infoShort.append(skippedLine) 1331 1332 infoText = "".join(info) 1333 infoTextShort = "".join(infoShort) 1334 1335 if show: 1336 uLogger.info(infoTextShort) 1337 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1338 1339 if self.searchResultsFile: 1340 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1341 fH.write(infoText) 1342 1343 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1344 1345 return searchResults 1346 1347 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1348 """ 1349 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1350 1351 :param instruments: list of strings with tickers or FIGIs. 1352 :return: list with unique instrument FIGIs only. 1353 """ 1354 requestedInstruments = [] 1355 for iName in instruments: 1356 if iName not in self.aliases.keys(): 1357 if iName not in requestedInstruments: 1358 requestedInstruments.append(iName) 1359 1360 else: 1361 if iName not in requestedInstruments: 1362 if self.aliases[iName] not in requestedInstruments: 1363 requestedInstruments.append(self.aliases[iName]) 1364 1365 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1366 1367 onlyUniqueFIGIs = [] 1368 for iName in requestedInstruments: 1369 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1370 continue 1371 1372 self.ticker = iName 1373 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1374 1375 if not iData: 1376 self.ticker = "" 1377 self.figi = iName 1378 1379 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1380 1381 if not iData: 1382 self.figi = "" 1383 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1384 1385 if iData and iData["figi"] not in onlyUniqueFIGIs: 1386 onlyUniqueFIGIs.append(iData["figi"]) 1387 1388 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1389 1390 return onlyUniqueFIGIs 1391 1392 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1393 """ 1394 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1395 See limits: https://tinkoff.github.io/investAPI/limits/ 1396 If `pricesFile` string is not empty then also save information to this file. 1397 1398 :param instruments: list of strings with tickers or FIGIs. 1399 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1400 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1401 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1402 """ 1403 if instruments is None or not instruments: 1404 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1405 raise Exception("Ticker or FIGI required") 1406 1407 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1408 1409 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1410 1411 iList = [] # trying to get info and current prices about all unique instruments: 1412 for self.figi in onlyUniqueFIGIs: 1413 iData = self.SearchByFIGI(requestPrice=True) 1414 iList.append(iData) 1415 1416 self.ShowListOfPrices(iList, show) 1417 1418 return iList 1419 1420 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1421 """ 1422 Show table contains current prices of given instruments. 1423 1424 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1425 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1426 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1427 :return: multilines text in Markdown format as a table contains current prices. 1428 """ 1429 infoText = "" 1430 1431 if show or self.pricesFile: 1432 info = [ 1433 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1434 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1435 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1436 ] 1437 1438 for item in iList: 1439 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1440 item["ticker"], 1441 item["figi"], 1442 item["type"], 1443 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1444 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1445 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1446 "{} / {}".format( 1447 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1448 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1449 ), 1450 "{} / {}".format( 1451 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1452 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1453 ), 1454 item["currency"], 1455 )) 1456 1457 infoText = "".join(info) 1458 1459 if show: 1460 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1461 1462 if self.pricesFile: 1463 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1464 fH.write(infoText) 1465 1466 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1467 1468 return infoText 1469 1470 def RequestTradingStatus(self) -> dict: 1471 """ 1472 Requesting trading status for the instrument defined by `figi` variable. 1473 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1474 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1475 1476 :return: dictionary with trading status attributes. Response example: 1477 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1478 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1479 """ 1480 if self.figi is None or not self.figi: 1481 uLogger.error("Variable `figi` must be defined for using this method!") 1482 raise Exception("FIGI required") 1483 1484 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1485 1486 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1487 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1488 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1489 1490 uLogger.debug("Records about current trading status successfully received") 1491 1492 return tradingStatus 1493 1494 def RequestPortfolio(self) -> dict: 1495 """ 1496 Requesting actual user's portfolio for current `accountId`. 1497 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1498 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1499 1500 :return: dictionary with user's portfolio. 1501 """ 1502 if self.accountId is None or not self.accountId: 1503 uLogger.error("Variable `accountId` must be defined for using this method!") 1504 raise Exception("Account ID required") 1505 1506 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1507 1508 self.body = str({"accountId": self.accountId}) 1509 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1510 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1511 1512 uLogger.debug("Records about user's portfolio successfully received") 1513 1514 return rawPortfolio 1515 1516 def RequestPositions(self) -> dict: 1517 """ 1518 Requesting open positions by currencies and instruments for current `accountId`. 1519 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1520 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1521 1522 :return: dictionary with open positions by instruments. 1523 """ 1524 if self.accountId is None or not self.accountId: 1525 uLogger.error("Variable `accountId` must be defined for using this method!") 1526 raise Exception("Account ID required") 1527 1528 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1529 1530 self.body = str({"accountId": self.accountId}) 1531 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1532 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1533 1534 uLogger.debug("Records about current open positions successfully received") 1535 1536 return rawPositions 1537 1538 def RequestPendingOrders(self) -> list: 1539 """ 1540 Requesting current actual pending orders for current `accountId`. 1541 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1542 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1543 1544 :return: list of dictionaries with pending orders. 1545 """ 1546 if self.accountId is None or not self.accountId: 1547 uLogger.error("Variable `accountId` must be defined for using this method!") 1548 raise Exception("Account ID required") 1549 1550 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1551 1552 self.body = str({"accountId": self.accountId}) 1553 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1554 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1555 1556 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1557 1558 return rawOrders 1559 1560 def RequestStopOrders(self) -> list: 1561 """ 1562 Requesting current actual stop orders for current `accountId`. 1563 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1564 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1565 1566 :return: list of dictionaries with stop orders. 1567 """ 1568 if self.accountId is None or not self.accountId: 1569 uLogger.error("Variable `accountId` must be defined for using this method!") 1570 raise Exception("Account ID required") 1571 1572 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1573 1574 self.body = str({"accountId": self.accountId}) 1575 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1576 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1577 1578 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1579 1580 return rawStopOrders 1581 1582 def Overview(self, show: bool = False, details: str = "full") -> dict: 1583 """ 1584 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1585 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1586 are defined then also save information to file. 1587 1588 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1589 many requests about the state of the portfolio, and then, based on the received data, a large number 1590 of calculation and statistics are collected. 1591 1592 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1593 :param details: how detailed should the information be? You should specify one of strings: 1594 `full` - shows full available information about portfolio status (by default), 1595 `positions` - shows only open positions, 1596 `digest` - show a short digest of the portfolio status, 1597 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1598 `orders` - shows only sections of open limits and stop orders. 1599 :return: dictionary with client's raw portfolio and some statistics. 1600 """ 1601 if self.accountId is None or not self.accountId: 1602 uLogger.error("Variable `accountId` must be defined for using this method!") 1603 raise Exception("Account ID required") 1604 1605 view = { 1606 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1607 "headers": {}, # list of dictionaries, response headers without "positions" section 1608 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1609 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1610 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1611 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1612 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1613 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1614 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1615 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1616 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1617 }, 1618 "stat": { # --- some statistics calculated using "raw" sections: 1619 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1620 "availableRUB": 0., # available rubles (without other currencies) 1621 "blockedRUB": 0., # blocked sum in Russian Rouble 1622 "totalChangesRUB": 0., # changes for all open trades in RUB 1623 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1624 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1625 "sharesCostRUB": 0., # costs of all shares in RUB 1626 "bondsCostRUB": 0., # costs of all bonds in RUB 1627 "etfsCostRUB": 0., # costs of all etfs in RUB 1628 "futuresCostRUB": 0., # costs of all futures in RUB 1629 "Currencies": [], # list of dictionaries of all currencies statistics 1630 "Shares": [], # list of dictionaries of all shares statistics 1631 "Bonds": [], # list of dictionaries of all bonds statistics 1632 "Etfs": [], # list of dictionaries of all etfs statistics 1633 "Futures": [], # list of dictionaries of all futures statistics 1634 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1635 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1636 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1637 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1638 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1639 }, 1640 "analytics": { # --- some analytics of portfolio: 1641 "distrByAssets": {}, # portfolio distribution by assets 1642 "distrByCompanies": {}, # portfolio distribution by companies 1643 "distrBySectors": {}, # portfolio distribution by sectors 1644 "distrByCurrencies": {}, # portfolio distribution by currencies 1645 "distrByCountries": {}, # portfolio distribution by countries 1646 } 1647 } 1648 1649 details = details.lower() 1650 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1651 if details not in availableDetails: 1652 details = "full" 1653 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1654 1655 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1656 1657 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1658 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1659 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1660 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1661 1662 # save response headers without "positions" section: 1663 for key in portfolioResponse.keys(): 1664 if key != "positions": 1665 view["raw"]["headers"][key] = portfolioResponse[key] 1666 1667 else: 1668 continue 1669 1670 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1671 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1672 for item in portfolioResponse["positions"]: 1673 if item["instrumentType"] == "currency": 1674 self.figi = item["figi"] 1675 curr = self.SearchByFIGI(requestPrice=False) 1676 1677 # current price of currency in RUB: 1678 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1679 "name": curr["name"], 1680 "currentPrice": NanoToFloat( 1681 item["currentPrice"]["units"], 1682 item["currentPrice"]["nano"] 1683 ), 1684 } 1685 1686 view["raw"]["Currencies"].append(item) 1687 1688 elif item["instrumentType"] == "share": 1689 view["raw"]["Shares"].append(item) 1690 1691 elif item["instrumentType"] == "bond": 1692 view["raw"]["Bonds"].append(item) 1693 1694 elif item["instrumentType"] == "etf": 1695 view["raw"]["Etfs"].append(item) 1696 1697 elif item["instrumentType"] == "futures": 1698 view["raw"]["Futures"].append(item) 1699 1700 else: 1701 continue 1702 1703 # how many volume of currencies (by ISO currency name) are blocked: 1704 for item in view["raw"]["positions"]["blocked"]: 1705 blocked = NanoToFloat(item["units"], item["nano"]) 1706 if blocked > 0: 1707 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1708 1709 # how many volume of instruments (by FIGI) are blocked: 1710 for item in view["raw"]["positions"]["securities"]: 1711 blocked = int(item["blocked"]) 1712 if blocked > 0: 1713 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1714 1715 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1716 1717 if "rub" in allBlocked.keys(): 1718 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1719 1720 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1721 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1722 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1723 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1724 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1725 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1726 view["stat"]["portfolioCostRUB"] = sum([ 1727 view["stat"]["allCurrenciesCostRUB"], 1728 view["stat"]["sharesCostRUB"], 1729 view["stat"]["bondsCostRUB"], 1730 view["stat"]["etfsCostRUB"], 1731 view["stat"]["futuresCostRUB"], 1732 ]) 1733 1734 # --- calculating some portfolio statistics: 1735 byComp = {} # distribution by companies 1736 bySect = {} # distribution by sectors 1737 byCurr = {} # distribution by currencies (include RUB) 1738 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1739 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1740 1741 for item in portfolioResponse["positions"]: 1742 self.figi = item["figi"] 1743 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1744 1745 if instrument: 1746 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1747 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1748 1749 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1750 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1751 1752 else: 1753 blocked = 0 1754 1755 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1756 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1757 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1758 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1759 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1760 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1761 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1762 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1763 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1764 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1765 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1766 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1767 1768 statData = { 1769 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1770 "ticker": instrument["ticker"], # ticker by FIGI 1771 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1772 "volume": volume, # available volume of instrument 1773 "lots": lots, # volume in lots of instrument 1774 "direction": direction, # direction of an instrument's position: short or long 1775 "blocked": blocked, # blocked volume of currency or instrument 1776 "currentPrice": curPrice, # current instrument's price in basic asset 1777 "average": average, # current average position price 1778 "cost": cost, # current cost of all volume of instrument in basic asset 1779 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1780 "costRUB": costRUB, # cost of instrument in ruble 1781 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1782 "profit": profit, # expected profit at current moment 1783 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1784 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1785 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1786 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1787 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1788 "step": instrument["step"], # minimum price increment 1789 } 1790 1791 # adding distribution by unique countries: 1792 if statData["country"] not in byCountry.keys(): 1793 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1794 1795 else: 1796 byCountry[statData["country"]]["cost"] += costRUB 1797 byCountry[statData["country"]]["percent"] += percentCostRUB 1798 1799 if item["instrumentType"] != "currency": 1800 # adding distribution by unique companies: 1801 if statData["name"]: 1802 if statData["name"] not in byComp.keys(): 1803 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1804 1805 else: 1806 byComp[statData["name"]]["cost"] += costRUB 1807 byComp[statData["name"]]["percent"] += percentCostRUB 1808 1809 # adding distribution by unique sectors: 1810 if statData["sector"] not in bySect.keys(): 1811 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1812 1813 else: 1814 bySect[statData["sector"]]["cost"] += costRUB 1815 bySect[statData["sector"]]["percent"] += percentCostRUB 1816 1817 # adding distribution by unique currencies: 1818 if currency not in byCurr.keys(): 1819 byCurr[currency] = { 1820 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1821 "cost": costRUB, 1822 "percent": percentCostRUB 1823 } 1824 1825 else: 1826 byCurr[currency]["cost"] += costRUB 1827 byCurr[currency]["percent"] += percentCostRUB 1828 1829 # saving statistics for every instrument: 1830 if item["instrumentType"] == "currency": 1831 view["stat"]["Currencies"].append(statData) 1832 1833 # update dict with free funds for trading (total - blocked) by currencies 1834 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1835 view["stat"]["funds"][currency] = { 1836 "total": volume, 1837 "totalCostRUB": costRUB, # total volume cost in rubles 1838 "free": volume - blocked, 1839 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1840 } 1841 1842 elif item["instrumentType"] == "share": 1843 view["stat"]["Shares"].append(statData) 1844 1845 elif item["instrumentType"] == "bond": 1846 view["stat"]["Bonds"].append(statData) 1847 1848 elif item["instrumentType"] == "etf": 1849 view["stat"]["Etfs"].append(statData) 1850 1851 elif item["instrumentType"] == "Futures": 1852 view["stat"]["Futures"].append(statData) 1853 1854 else: 1855 continue 1856 1857 # total changes in Russian Ruble: 1858 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1859 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1860 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1861 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1862 view["stat"]["funds"]["rub"] = { 1863 "total": view["stat"]["availableRUB"], 1864 "totalCostRUB": view["stat"]["availableRUB"], 1865 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1866 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1867 } 1868 1869 # --- pending orders sector data: 1870 uniquePendingOrders = [] 1871 uniquePendingOrdersFIGIs = [] 1872 for item in view["raw"]["orders"]: 1873 if item["figi"] not in uniquePendingOrdersFIGIs: 1874 uniquePendingOrdersFIGIs.append(item["figi"]) 1875 uniquePendingOrders.append(item) 1876 1877 for item in uniquePendingOrders: 1878 self.figi = item["figi"] 1879 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1880 1881 if instrument: 1882 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1883 orderType = TKS_ORDER_TYPES[item["orderType"]] 1884 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1885 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1886 1887 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1888 if item["direction"] == "ORDER_DIRECTION_BUY": 1889 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1890 1891 else: 1892 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1893 1894 # requested price for order execution: 1895 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1896 1897 # necessary changes in percent to reach target from current price: 1898 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1899 1900 view["stat"]["orders"].append({ 1901 "orderID": item["orderId"], # orderId number parameter of current order 1902 "figi": item["figi"], # FIGI identification 1903 "ticker": instrument["ticker"], # ticker name by FIGI 1904 "lotsRequested": item["lotsRequested"], # requested lots value 1905 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1906 "currentPrice": lastPrice, # current instrument's price for defined action 1907 "targetPrice": target, # requested price for order execution in base currency 1908 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1909 "percentChanges": changes, # changes in percent to target from current price 1910 "currency": item["currency"], # instrument's currency name 1911 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1912 "type": orderType, # type of order from TKS_ORDER_TYPES 1913 "status": orderState, # order status from TKS_ORDER_STATES 1914 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1915 }) 1916 1917 # --- stop orders sector data: 1918 uniqueStopOrders = [] 1919 uniqueStopOrdersFIGIs = [] 1920 for item in view["raw"]["stopOrders"]: 1921 if item["figi"] not in uniqueStopOrdersFIGIs: 1922 uniqueStopOrdersFIGIs.append(item["figi"]) 1923 uniqueStopOrders.append(item) 1924 1925 for item in uniqueStopOrders: 1926 self.figi = item["figi"] 1927 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1928 1929 if instrument: 1930 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1931 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1932 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1933 1934 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1935 if "expirationTime" in item.keys(): 1936 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1937 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1938 1939 else: 1940 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1941 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1942 1943 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1944 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1945 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1946 1947 else: 1948 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1949 1950 # requested price when stop-order executed: 1951 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1952 1953 # price for limit-order, set up when stop-order executed: 1954 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1955 1956 # necessary changes in percent to reach target from current price: 1957 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1958 1959 view["stat"]["stopOrders"].append({ 1960 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1961 "figi": item["figi"], # FIGI identification 1962 "ticker": instrument["ticker"], # ticker name by FIGI 1963 "lotsRequested": item["lotsRequested"], # requested lots value 1964 "currentPrice": lastPrice, # current instrument's price for defined action 1965 "targetPrice": target, # requested price for stop-order execution in base currency 1966 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1967 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1968 "percentChanges": changes, # changes in percent to target from current price 1969 "currency": item["currency"], # instrument's currency name 1970 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1971 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1972 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1973 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1974 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1975 }) 1976 1977 # --- calculating data for analytics section: 1978 # portfolio distribution by assets: 1979 view["analytics"]["distrByAssets"] = { 1980 "Ruble": { 1981 "uniques": 1, 1982 "cost": view["stat"]["availableRUB"], 1983 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1984 }, 1985 "Currencies": { 1986 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1987 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1988 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1989 }, 1990 "Shares": { 1991 "uniques": len(view["stat"]["Shares"]), 1992 "cost": view["stat"]["sharesCostRUB"], 1993 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1994 }, 1995 "Bonds": { 1996 "uniques": len(view["stat"]["Bonds"]), 1997 "cost": view["stat"]["bondsCostRUB"], 1998 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1999 }, 2000 "Etfs": { 2001 "uniques": len(view["stat"]["Etfs"]), 2002 "cost": view["stat"]["etfsCostRUB"], 2003 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2004 }, 2005 "Futures": { 2006 "uniques": len(view["stat"]["Futures"]), 2007 "cost": view["stat"]["futuresCostRUB"], 2008 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2009 }, 2010 } 2011 2012 # portfolio distribution by companies: 2013 view["analytics"]["distrByCompanies"]["All money cash"] = { 2014 "ticker": "", 2015 "cost": view["stat"]["allCurrenciesCostRUB"], 2016 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2017 } 2018 view["analytics"]["distrByCompanies"].update(byComp) 2019 2020 # portfolio distribution by sectors: 2021 view["analytics"]["distrBySectors"]["All money cash"] = { 2022 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2023 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2024 } 2025 view["analytics"]["distrBySectors"].update(bySect) 2026 2027 # portfolio distribution by currencies: 2028 view["analytics"]["distrByCurrencies"].update(byCurr) 2029 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2030 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2031 2032 # portfolio distribution by countries: 2033 view["analytics"]["distrByCountries"].update(byCountry) 2034 2035 # --- Prepare text statistics overview in human-readable: 2036 if show: 2037 # Whatever the value `details`, header not changes: 2038 info = [ 2039 "# Client's portfolio\n\n", 2040 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2041 "* **Account ID:** [{}]\n".format(self.accountId), 2042 ] 2043 2044 if details in ["full", "positions", "digest"]: 2045 info.extend([ 2046 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2047 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2048 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2049 view["stat"]["totalChangesRUB"], 2050 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2051 view["stat"]["totalChangesPercentRUB"], 2052 ), 2053 ]) 2054 2055 if details in ["full", "positions"]: 2056 info.extend([ 2057 "## Open positions\n\n", 2058 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2059 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2060 "| Ruble | {:>31} | | | | | |\n".format( 2061 "{:.2f} ({:.2f}) rub".format( 2062 view["stat"]["availableRUB"], 2063 view["stat"]["blockedRUB"], 2064 ) 2065 ) 2066 ]) 2067 2068 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2069 return [ 2070 "| | | | | | | |\n", 2071 "| {:<27} | | | | | {:>19} | |\n".format( 2072 noTradeStr if noTradeStr else typeStr, 2073 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2074 ), 2075 ] 2076 2077 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2078 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2079 "{} [{}]".format(data["ticker"], data["figi"]), 2080 "{:.2f} ({:.2f}) {}".format( 2081 data["volume"], 2082 data["blocked"], 2083 data["currency"], 2084 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2085 data["volume"], 2086 data["blocked"], 2087 ), 2088 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2089 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2090 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2091 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2092 "{}{:.2f} {} ({}{:.2f}%)".format( 2093 "+" if data["profit"] > 0 else "", 2094 data["profit"], data["baseCurrencyName"], 2095 "+" if data["percentProfit"] > 0 else "", 2096 data["percentProfit"], 2097 ), 2098 ) 2099 2100 # --- Show currencies section: 2101 if view["stat"]["Currencies"]: 2102 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2103 for item in view["stat"]["Currencies"]: 2104 info.append(_InfoStr(item, showCurrencyName=True)) 2105 2106 else: 2107 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2108 2109 # --- Show shares section: 2110 if view["stat"]["Shares"]: 2111 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2112 2113 for item in view["stat"]["Shares"]: 2114 info.append(_InfoStr(item)) 2115 2116 else: 2117 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2118 2119 # --- Show bonds section: 2120 if view["stat"]["Bonds"]: 2121 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2122 2123 for item in view["stat"]["Bonds"]: 2124 info.append(_InfoStr(item)) 2125 2126 else: 2127 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2128 2129 # --- Show etfs section: 2130 if view["stat"]["Etfs"]: 2131 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2132 2133 for item in view["stat"]["Etfs"]: 2134 info.append(_InfoStr(item)) 2135 2136 else: 2137 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2138 2139 # --- Show futures section: 2140 if view["stat"]["Futures"]: 2141 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2142 2143 for item in view["stat"]["Futures"]: 2144 info.append(_InfoStr(item)) 2145 2146 else: 2147 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2148 2149 if details in ["full", "orders"]: 2150 # --- Show pending orders section: 2151 if view["stat"]["orders"]: 2152 info.extend([ 2153 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2154 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2155 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2156 ]) 2157 2158 for item in view["stat"]["orders"]: 2159 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2160 "{} [{}]".format(item["ticker"], item["figi"]), 2161 item["orderID"], 2162 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2163 "{} {} ({}{:.2f}%)".format( 2164 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2165 item["baseCurrencyName"], 2166 "+" if item["percentChanges"] > 0 else "", 2167 float(item["percentChanges"]), 2168 ), 2169 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2170 item["action"], 2171 item["type"], 2172 item["date"], 2173 )) 2174 2175 else: 2176 info.append("\n## Total pending limit-orders: 0\n") 2177 2178 # --- Show stop orders section: 2179 if view["stat"]["stopOrders"]: 2180 info.extend([ 2181 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2182 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2183 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2184 ]) 2185 2186 for item in view["stat"]["stopOrders"]: 2187 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2188 "{} [{}]".format(item["ticker"], item["figi"]), 2189 item["orderID"], 2190 item["lotsRequested"], 2191 "{} {} ({}{:.2f}%)".format( 2192 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2193 item["baseCurrencyName"], 2194 "+" if item["percentChanges"] > 0 else "", 2195 float(item["percentChanges"]), 2196 ), 2197 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2198 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2199 item["action"], 2200 item["type"], 2201 item["expType"], 2202 item["createDate"], 2203 item["expDate"], 2204 )) 2205 2206 else: 2207 info.append("\n## Total stop-orders: 0\n") 2208 2209 if details in ["full", "analytics"]: 2210 # -- Show analytics section: 2211 if view["stat"]["portfolioCostRUB"] > 0: 2212 info.extend([ 2213 "\n# Analytics\n" 2214 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2215 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2216 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2217 view["stat"]["totalChangesRUB"], 2218 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2219 view["stat"]["totalChangesPercentRUB"], 2220 ), 2221 "\n## Portfolio distribution by assets\n" 2222 "\n| Type | Uniques | Percent | Current cost |\n", 2223 "|------------|---------|---------|--------------------|\n", 2224 ]) 2225 2226 for key in view["analytics"]["distrByAssets"].keys(): 2227 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2228 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2229 key, 2230 view["analytics"]["distrByAssets"][key]["uniques"], 2231 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2232 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2233 )) 2234 2235 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2236 info.extend([ 2237 "\n## Portfolio distribution by companies\n" 2238 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2239 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2240 ]) 2241 2242 for company in view["analytics"]["distrByCompanies"].keys(): 2243 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2244 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2245 info.append("| {} | {:<7} | {:<18} |\n".format( 2246 "{}{}{}".format( 2247 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2248 company, 2249 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2250 ), 2251 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2252 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2253 )) 2254 2255 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2256 info.extend([ 2257 "\n## Portfolio distribution by sectors\n" 2258 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2259 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2260 ]) 2261 2262 for sector in view["analytics"]["distrBySectors"].keys(): 2263 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2264 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2265 sector, 2266 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2267 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2268 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2269 )) 2270 2271 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2272 info.extend([ 2273 "\n## Portfolio distribution by currencies\n" 2274 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2275 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2276 ]) 2277 2278 for curr in view["analytics"]["distrByCurrencies"].keys(): 2279 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2280 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2281 info.append("| {} | {:<7} | {:<18} |\n".format( 2282 "[{}] {}{}".format( 2283 curr, 2284 view["analytics"]["distrByCurrencies"][curr]["name"], 2285 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2286 ), 2287 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2288 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2289 )) 2290 2291 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2292 info.extend([ 2293 "\n## Portfolio distribution by countries\n" 2294 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2295 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2296 ]) 2297 2298 for country in view["analytics"]["distrByCountries"].keys(): 2299 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2300 nameLen = len(country) 2301 info.append("| {} | {:<7} | {:<18} |\n".format( 2302 "{}{}".format( 2303 country, 2304 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2305 ), 2306 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2307 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2308 )) 2309 2310 infoText = "".join(info) 2311 2312 uLogger.info(infoText) 2313 2314 if details == "full" and self.overviewFile: 2315 filename = self.overviewFile 2316 2317 elif details == "digest" and self.overviewDigestFile: 2318 filename = self.overviewDigestFile 2319 2320 elif details == "positions" and self.overviewPositionsFile: 2321 filename = self.overviewPositionsFile 2322 2323 elif details == "orders" and self.overviewOrdersFile: 2324 filename = self.overviewOrdersFile 2325 2326 elif details == "analytics" and self.overviewAnalyticsFile: 2327 filename = self.overviewAnalyticsFile 2328 2329 else: 2330 filename = "" 2331 2332 if filename: 2333 with open(filename, "w", encoding="UTF-8") as fH: 2334 fH.write(infoText) 2335 2336 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2337 2338 return view 2339 2340 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2341 """ 2342 Returns history operations between two given dates for current `accountId`. 2343 If `reportFile` string is not empty then also save human-readable report. 2344 Shows some statistical data of closed positions. 2345 2346 :param start: see docstring in `GetDatesAsString()` method 2347 :param end: see docstring in `GetDatesAsString()` method 2348 :param show: if `True` then also prints all records to the console. 2349 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2350 :return: original list of dictionaries with history of deals records from API ("operations" key): 2351 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2352 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2353 """ 2354 if self.accountId is None or not self.accountId: 2355 uLogger.error("Variable `accountId` must be defined for using this method!") 2356 raise Exception("Account ID required") 2357 2358 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2359 2360 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2361 2362 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2363 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2364 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2365 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2366 customStat = {} # custom statistics in additional to responseJSON 2367 2368 # --- output report in human-readable format: 2369 if show or self.reportFile: 2370 splitLine1 = "| | | | | |\n" # Summary section 2371 splitLine2 = "| | | | | | | | |\n" # Operations section 2372 nextDay = "" 2373 2374 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2375 2376 if len(ops) > 0: 2377 customStat = { 2378 "opsCount": 0, # total operations count 2379 "buyCount": 0, # buy operations 2380 "sellCount": 0, # sell operations 2381 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2382 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2383 "payIn": {"rub": 0.}, # Deposit brokerage account 2384 "payOut": {"rub": 0.}, # Withdrawals 2385 "divs": {"rub": 0.}, # Dividends income 2386 "coupons": {"rub": 0.}, # Coupon's income 2387 "brokerCom": {"rub": 0.}, # Service commissions 2388 "serviceCom": {"rub": 0.}, # Service commissions 2389 "marginCom": {"rub": 0.}, # Margin commissions 2390 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2391 } 2392 2393 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2394 for item in ops: 2395 if item["state"] == "OPERATION_STATE_EXECUTED": 2396 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2397 2398 # count buy operations: 2399 if "_BUY" in item["operationType"]: 2400 customStat["buyCount"] += 1 2401 2402 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2403 customStat["buyTotal"][item["payment"]["currency"]] += payment 2404 2405 else: 2406 customStat["buyTotal"][item["payment"]["currency"]] = payment 2407 2408 # count sell operations: 2409 elif "_SELL" in item["operationType"]: 2410 customStat["sellCount"] += 1 2411 2412 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2413 customStat["sellTotal"][item["payment"]["currency"]] += payment 2414 2415 else: 2416 customStat["sellTotal"][item["payment"]["currency"]] = payment 2417 2418 # count incoming operations: 2419 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2420 if item["payment"]["currency"] in customStat["payIn"].keys(): 2421 customStat["payIn"][item["payment"]["currency"]] += payment 2422 2423 else: 2424 customStat["payIn"][item["payment"]["currency"]] = payment 2425 2426 # count withdrawals operations: 2427 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2428 if item["payment"]["currency"] in customStat["payOut"].keys(): 2429 customStat["payOut"][item["payment"]["currency"]] += payment 2430 2431 else: 2432 customStat["payOut"][item["payment"]["currency"]] = payment 2433 2434 # count dividends income: 2435 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2436 if item["payment"]["currency"] in customStat["divs"].keys(): 2437 customStat["divs"][item["payment"]["currency"]] += payment 2438 2439 else: 2440 customStat["divs"][item["payment"]["currency"]] = payment 2441 2442 # count coupon's income: 2443 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2444 if item["payment"]["currency"] in customStat["coupons"].keys(): 2445 customStat["coupons"][item["payment"]["currency"]] += payment 2446 2447 else: 2448 customStat["coupons"][item["payment"]["currency"]] = payment 2449 2450 # count broker commissions: 2451 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2452 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2453 customStat["brokerCom"][item["payment"]["currency"]] += payment 2454 2455 else: 2456 customStat["brokerCom"][item["payment"]["currency"]] = payment 2457 2458 # count service commissions: 2459 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2460 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2461 customStat["serviceCom"][item["payment"]["currency"]] += payment 2462 2463 else: 2464 customStat["serviceCom"][item["payment"]["currency"]] = payment 2465 2466 # count margin commissions: 2467 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2468 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2469 customStat["marginCom"][item["payment"]["currency"]] += payment 2470 2471 else: 2472 customStat["marginCom"][item["payment"]["currency"]] = payment 2473 2474 # count withholding taxes: 2475 elif "_TAX" in item["operationType"]: 2476 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2477 customStat["allTaxes"][item["payment"]["currency"]] += payment 2478 2479 else: 2480 customStat["allTaxes"][item["payment"]["currency"]] = payment 2481 2482 else: 2483 continue 2484 2485 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2486 2487 # --- view "Actions" lines: 2488 info.extend([ 2489 "| 1 | 2 | 3 | 4 | 5 |\n", 2490 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2491 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2492 "| | Buy: {:<22} | {:<28} | | |\n".format( 2493 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2494 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2495 ), 2496 "| | Sell: {:<21} | {:<28} | | |\n".format( 2497 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2498 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2499 ), 2500 ]) 2501 2502 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2503 for key in opsKeys: 2504 if key == "rub": 2505 continue 2506 2507 info.extend([ 2508 "| | | {:<28} | | |\n".format( 2509 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2510 ), 2511 "| | | {:<28} | | |\n".format( 2512 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2513 ), 2514 ]) 2515 2516 info.append(splitLine1) 2517 2518 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2519 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2520 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2521 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2522 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2523 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2524 ) 2525 2526 # --- view "Payments" lines: 2527 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2528 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2529 2530 for key in paymentsKeys: 2531 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2532 2533 info.append(splitLine1) 2534 2535 # --- view "Commissions and taxes" lines: 2536 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2537 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2538 2539 for key in comKeys: 2540 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2541 2542 info.append(splitLine1) 2543 2544 info.extend([ 2545 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2546 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2547 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2548 ]) 2549 2550 else: 2551 info.append("Broker returned no operations during this period\n") 2552 2553 # --- view "Operations" section: 2554 for item in ops: 2555 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2556 continue 2557 2558 else: 2559 self.figi = item["figi"] if item["figi"] else "" 2560 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2561 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2562 2563 # group of deals during one day: 2564 if nextDay and item["date"].split("T")[0] != nextDay: 2565 info.append(splitLine2) 2566 nextDay = "" 2567 2568 else: 2569 nextDay = item["date"].split("T")[0] # saving current day for splitting 2570 2571 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2572 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2573 self.figi if self.figi else "—", 2574 instrument["ticker"] if instrument else "—", 2575 instrument["type"] if instrument else "—", 2576 item["quantity"] if int(item["quantity"]) > 0 else "—", 2577 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2578 TKS_OPERATION_STATES[item["state"]], 2579 TKS_OPERATION_TYPES[item["operationType"]], 2580 )) 2581 2582 infoText = "".join(info) 2583 2584 if show: 2585 uLogger.info(infoText) 2586 2587 if self.reportFile: 2588 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2589 fH.write(infoText) 2590 2591 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2592 2593 return ops, customStat 2594 2595 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2596 """ 2597 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2598 2599 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2600 Warning! Broker server used ISO UTC time by default. 2601 2602 If `historyFile` is not `None` then method save history to file, otherwise return only pandas dataframe. 2603 Also, `historyFile` used to update history with `onlyMissing` parameter. 2604 2605 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2606 2607 :param start: see docstring in `GetDatesAsString()` method. 2608 :param end: see docstring in `GetDatesAsString()` method. 2609 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2610 `"hour"`, `"day"`. Default: `"hour"`. 2611 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2612 False by default. Warning! History appends only from last candle to current time 2613 with always update last candle! 2614 :param csvSep: separator if csv-file is used, `,` by default. 2615 :param show: if `True` then also prints pandas dataframe to the console. 2616 :return: pandas dataframe with prices history. Headers of columns are defined by default: 2617 `["date", "time", "open", "high", "low", "close", "volume"]`. 2618 """ 2619 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2620 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2621 history = None # empty pandas object for history 2622 2623 if interval not in TKS_CANDLE_INTERVALS.keys(): 2624 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2625 raise Exception("Incorrect value") 2626 2627 if not (self.ticker or self.figi): 2628 uLogger.error("Ticker or FIGI must be defined!") 2629 raise Exception("Ticker or FIGI required") 2630 2631 if self.ticker and not self.figi: 2632 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2633 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2634 2635 if self.figi and not self.ticker: 2636 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2637 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2638 2639 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2640 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2641 if interval.lower() != "day": 2642 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2643 2644 delta = dtEnd - dtStart # current UTC time minus last time in file 2645 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2646 2647 # calculate history length in candles: 2648 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2649 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2650 length += 1 # to avoid fraction time 2651 2652 # calculate data blocks count: 2653 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2654 2655 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2656 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2657 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2658 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2659 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2660 2661 tempOld = None # pandas object for old history, if --only-missing key present 2662 lastTime = None # datetime object of last old candle in file 2663 2664 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2665 uLogger.debug("--only-missing key present, add only last missing candles...") 2666 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2667 2668 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2669 2670 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2671 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2672 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2673 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2674 2675 # get last datetime object from last string in file or minus 1 delta if file is empty: 2676 if len(tempOld) > 0: 2677 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2678 2679 else: 2680 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2681 2682 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2683 2684 responseJSONs = [] # raw history blocks of data 2685 2686 blockEnd = dtEnd 2687 for item in range(blocks): 2688 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2689 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2690 2691 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2692 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2693 )) 2694 2695 if blockStart == blockEnd: 2696 uLogger.debug("Skipped this zero-length block...") 2697 2698 else: 2699 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2700 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2701 self.body = str({ 2702 "figi": self.figi, 2703 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2704 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2705 "interval": TKS_CANDLE_INTERVALS[interval][0] 2706 }) 2707 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2708 2709 if "code" in responseJSON.keys(): 2710 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2711 2712 else: 2713 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2714 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2715 2716 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2717 2718 blockEnd = blockStart 2719 2720 printCount = len(responseJSONs) # candles to show in console 2721 if responseJSONs: 2722 tempHistory = pd.DataFrame( 2723 data={ 2724 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2725 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2726 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2727 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2728 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2729 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2730 "volume": [int(item["volume"]) for item in responseJSONs], 2731 }, 2732 index=range(len(responseJSONs)), 2733 columns=["date", "time", "open", "high", "low", "close", "volume"], 2734 ) 2735 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2736 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2737 2738 # append only newest candles to old history if --only-missing key present: 2739 if onlyMissing and tempOld is not None and lastTime is not None: 2740 index = 0 # find start index in tempHistory data: 2741 2742 for i, item in tempHistory.iterrows(): 2743 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2744 2745 if curTime == lastTime: 2746 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2747 index = i 2748 printCount = index + 1 2749 break 2750 2751 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2752 2753 else: 2754 history = tempHistory # if no `--only-missing` key then load full data from server 2755 2756 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2757 2758 if history is not None and not history.empty: 2759 if show: 2760 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2761 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2762 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2763 )) 2764 2765 else: 2766 uLogger.warning("Received an empty candles history!") 2767 2768 if self.historyFile is not None: 2769 if history is not None and not history.empty: 2770 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2771 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2772 2773 else: 2774 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2775 2776 else: 2777 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only pandas dataframe returns.") 2778 2779 return history 2780 2781 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2782 """ 2783 Load candles history from csv-file and return pandas dataframe object. 2784 2785 See also: `History()` and `ShowHistoryChart()` methods. 2786 2787 :param filePath: path to csv-file to open. 2788 """ 2789 loadedHistory = None # init candles data object 2790 2791 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2792 2793 if os.path.exists(filePath): 2794 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as pandas dataframe 2795 2796 tfStr = self.priceModel.FormattedDelta( 2797 self.priceModel.timeframe, 2798 "{days} days {hours}h {minutes}m {seconds}s", 2799 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2800 self.priceModel.timeframe, 2801 "{hours}h {minutes}m {seconds}s", 2802 ) 2803 2804 if loadedHistory is not None and not loadedHistory.empty: 2805 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2806 len(loadedHistory), 2807 tfStr, 2808 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2809 ) 2810 2811 else: 2812 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2813 2814 else: 2815 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2816 2817 return loadedHistory 2818 2819 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2820 """ 2821 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2822 2823 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2824 Default: `index.html` (both for interact and non-interact candlesticks chart). 2825 2826 See also: `History()` and `LoadHistory()` methods. 2827 2828 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2829 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2830 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2831 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2832 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2833 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2834 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2835 """ 2836 if isinstance(candles, str): 2837 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2838 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2839 2840 elif isinstance(candles, pd.DataFrame): 2841 self.priceModel.prices = candles # set candles chain from variable 2842 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2843 2844 if "datetime" not in candles.columns: 2845 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2846 2847 else: 2848 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2849 raise Exception("Incorrect value") 2850 2851 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2852 2853 if interact: 2854 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2855 2856 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2857 2858 else: 2859 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2860 2861 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2862 2863 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2864 2865 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2866 """ 2867 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2868 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2869 2870 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2871 2872 :param operation: string "Buy" or "Sell". 2873 :param lots: volume, integer count of lots >= 1. 2874 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2875 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2876 :param expDate: string "Undefined" by default or local date in future, 2877 it is a string with format `%Y-%m-%d %H:%M:%S`. 2878 :return: JSON with response from broker server. 2879 """ 2880 if self.accountId is None or not self.accountId: 2881 uLogger.error("Variable `accountId` must be defined for using this method!") 2882 raise Exception("Account ID required") 2883 2884 if operation is None or not operation or operation not in ("Buy", "Sell"): 2885 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2886 raise Exception("Incorrect value") 2887 2888 if lots is None or lots < 1: 2889 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2890 lots = 1 2891 2892 if tp is None or tp < 0: 2893 tp = 0 2894 2895 if sl is None or sl < 0: 2896 sl = 0 2897 2898 if expDate is None or not expDate: 2899 expDate = "Undefined" 2900 2901 if not (self.ticker or self.figi): 2902 uLogger.error("Ticker or FIGI must be defined!") 2903 raise Exception("Ticker or FIGI required") 2904 2905 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2906 self.ticker = instrument["ticker"] 2907 self.figi = instrument["figi"] 2908 2909 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2910 2911 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2912 self.body = str({ 2913 "figi": self.figi, 2914 "quantity": str(lots), 2915 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2916 "accountId": str(self.accountId), 2917 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2918 }) 2919 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2920 2921 if "orderId" in response.keys(): 2922 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2923 operation, response["orderId"], 2924 self.ticker, self.figi, lots, 2925 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2926 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2927 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2928 )) 2929 2930 else: 2931 uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.") 2932 2933 if tp > 0: 2934 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2935 2936 if sl > 0: 2937 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2938 2939 return response 2940 2941 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2942 """ 2943 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2944 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2945 2946 See also: `Order()` and `Trade()` docstrings. 2947 2948 :param lots: volume, integer count of lots >= 1. 2949 :param tp: float > 0, take profit price of stop-order. 2950 :param sl: float > 0, stop loss price of stop-order. 2951 :param expDate: it's a local date in future. 2952 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2953 :return: JSON with response from broker server. 2954 """ 2955 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2956 2957 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2958 """ 2959 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2960 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2961 2962 See also: `Order()` and `Trade()` docstrings. 2963 2964 :param lots: volume, integer count of lots >= 1. 2965 :param tp: float > 0, take profit price of stop-order. 2966 :param sl: float > 0, stop loss price of stop-order. 2967 :param expDate: it's a local date in the future. 2968 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2969 :return: JSON with response from broker server. 2970 """ 2971 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 2972 2973 def CloseTrades(self, tickers: list, portfolio: dict = None) -> None: 2974 """ 2975 Close position of given instruments. 2976 2977 :param tickers: tickers list of instruments that must be closed. 2978 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2979 This avoids unnecessary downloading data from the server. 2980 """ 2981 if not tickers: 2982 uLogger.info("Tickers list is empty, nothing to close.") 2983 2984 else: 2985 if portfolio is None or not portfolio: 2986 portfolio = self.Overview(show=False) 2987 2988 allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2989 uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers)) 2990 2991 for ticker in tickers: 2992 if ticker not in allOpenedTickers: 2993 uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker)) 2994 continue 2995 2996 # search open trade info about instrument by ticker: 2997 instrument = {} 2998 for iType in TKS_INSTRUMENTS: 2999 if instrument: 3000 break 3001 3002 for item in portfolio["stat"][iType]: 3003 if item["ticker"] == ticker: 3004 instrument = item 3005 break 3006 3007 if instrument: 3008 self.ticker = ticker 3009 self.figi = instrument["figi"] 3010 3011 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3012 self.ticker, 3013 self.figi, 3014 int(instrument["volume"]), 3015 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3016 )) 3017 3018 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3019 3020 if tradeLots > 0: 3021 if instrument["blocked"] > 0: 3022 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3023 instrument["blocked"], 3024 self.ticker, 3025 tradeLots, 3026 )) 3027 3028 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3029 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3030 3031 else: 3032 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3033 3034 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3035 """ 3036 Close all positions of given instruments with defined type. 3037 3038 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3039 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3040 This avoids unnecessary downloading data from the server. 3041 """ 3042 if iType not in TKS_INSTRUMENTS: 3043 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3044 3045 else: 3046 if portfolio is None or not portfolio: 3047 portfolio = self.Overview(show=False) 3048 3049 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3050 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3051 3052 if tickers and portfolio: 3053 self.CloseTrades(tickers, portfolio) 3054 3055 else: 3056 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3057 3058 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3059 """ 3060 Universal method to create market or limit orders with all available parameters for current `accountId`. 3061 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3062 3063 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3064 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3065 3066 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3067 then broker immediately open market order as you can do simple --buy or --sell operations! 3068 3069 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3070 When current price will go up or down to target price value then broker opens a limit order. 3071 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3072 3073 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3074 3075 :param operation: string "Buy" or "Sell". 3076 :param orderType: string "Limit" or "Stop". 3077 :param lots: volume, integer count of lots >= 1. 3078 :param targetPrice: target price > 0. This is open trade price for limit order. 3079 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3080 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3081 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3082 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3083 Stop loss order always executed by market price. 3084 :param expDate: string "Undefined" by default or local date in future. 3085 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3086 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3087 A limit order has no expiration date, it lasts until the end of the trading day. 3088 :return: JSON with response from broker server. 3089 """ 3090 if self.accountId is None or not self.accountId: 3091 uLogger.error("Variable `accountId` must be defined for using this method!") 3092 raise Exception("Account ID required") 3093 3094 if operation is None or not operation or operation not in ("Buy", "Sell"): 3095 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3096 raise Exception("Incorrect value") 3097 3098 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3099 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3100 raise Exception("Incorrect value") 3101 3102 if lots is None or lots < 1: 3103 uLogger.error("You must define trade volume > 0: integer count of lots!") 3104 raise Exception("Incorrect value") 3105 3106 if targetPrice is None or targetPrice <= 0: 3107 uLogger.error("Target price for limit-order must be greater than 0!") 3108 raise Exception("Incorrect value") 3109 3110 if limitPrice is None or limitPrice <= 0: 3111 limitPrice = targetPrice 3112 3113 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3114 stopType = "Limit" 3115 3116 if expDate is None or not expDate: 3117 expDate = "Undefined" 3118 3119 if not (self.ticker or self.figi): 3120 uLogger.error("Tocker or FIGI must be defined!") 3121 raise Exception("Ticker or FIGI required") 3122 3123 response = {} 3124 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3125 self.ticker = instrument["ticker"] 3126 self.figi = instrument["figi"] 3127 3128 if orderType == "Limit": 3129 uLogger.debug( 3130 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3131 self.ticker, self.figi, 3132 operation, lots, targetPrice, instrument["currency"], 3133 )) 3134 3135 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3136 self.body = str({ 3137 "figi": self.figi, 3138 "quantity": str(lots), 3139 "price": FloatToNano(targetPrice), 3140 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3141 "accountId": str(self.accountId), 3142 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3143 }) 3144 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3145 3146 if "orderId" in response.keys(): 3147 uLogger.info( 3148 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3149 response["orderId"], 3150 self.ticker, self.figi, 3151 operation, lots, targetPrice, instrument["currency"], 3152 )) 3153 3154 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3155 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3156 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3157 targetPrice, instrument["currency"], 3158 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3159 )) 3160 3161 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3162 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3163 targetPrice, instrument["currency"], 3164 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3165 )) 3166 3167 else: 3168 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3169 3170 if orderType == "Stop": 3171 uLogger.debug( 3172 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3173 self.ticker, self.figi, 3174 operation, lots, 3175 targetPrice, instrument["currency"], 3176 limitPrice, instrument["currency"], 3177 stopType, expDate, 3178 )) 3179 3180 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3181 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3182 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3183 3184 body = { 3185 "figi": self.figi, 3186 "quantity": str(lots), 3187 "price": FloatToNano(limitPrice), 3188 "stopPrice": FloatToNano(targetPrice), 3189 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3190 "accountId": str(self.accountId), 3191 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3192 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3193 } 3194 3195 if expDateUTC: 3196 body["expireDate"] = expDateUTC 3197 3198 self.body = str(body) 3199 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3200 3201 if "stopOrderId" in response.keys(): 3202 uLogger.info( 3203 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3204 response["stopOrderId"], 3205 self.ticker, self.figi, 3206 operation, lots, 3207 targetPrice, instrument["currency"], 3208 limitPrice, instrument["currency"], 3209 TKS_STOP_ORDER_TYPES[stopOrderType], 3210 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3211 )) 3212 3213 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3214 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3215 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3216 targetPrice, instrument["currency"], 3217 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3218 )) 3219 3220 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3221 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3222 targetPrice, instrument["currency"], 3223 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3224 )) 3225 3226 else: 3227 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3228 3229 return response 3230 3231 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3232 """ 3233 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3234 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3235 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3236 See also: `Order()` docstring. 3237 3238 :param lots: volume, integer count of lots >= 1. 3239 :param targetPrice: target price > 0. This is open trade price for limit order. 3240 :return: JSON with response from broker server. 3241 """ 3242 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3243 3244 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3245 """ 3246 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3247 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3248 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3249 target price value then broker opens a limit order. See also: `Order()` docstring. 3250 3251 :param lots: volume, integer count of lots >= 1. 3252 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3253 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3254 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3255 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3256 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3257 :param expDate: string "Undefined" by default or local date in future. 3258 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3259 This date is converting to UTC format for server. 3260 :return: JSON with response from broker server. 3261 """ 3262 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3263 3264 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3265 """ 3266 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3267 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3268 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3269 See also: `Order()` docstring. 3270 3271 :param lots: volume, integer count of lots >= 1. 3272 :param targetPrice: target price > 0. This is open trade price for limit order. 3273 :return: JSON with response from broker server. 3274 """ 3275 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3276 3277 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3278 """ 3279 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3280 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3281 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3282 target price value then broker opens a limit order. See also: `Order()` docstring. 3283 3284 :param lots: volume, integer count of lots >= 1. 3285 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3286 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3287 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3288 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3289 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3290 :param expDate: string "Undefined" by default or local date in future. 3291 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3292 This date is converting to UTC format for server. 3293 :return: JSON with response from broker server. 3294 """ 3295 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3296 3297 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3298 """ 3299 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3300 3301 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3302 :param allOrdersIDs: pre-received lists of all active pending orders. 3303 This avoids unnecessary downloading data from the server. 3304 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3305 """ 3306 if self.accountId is None or not self.accountId: 3307 uLogger.error("Variable `accountId` must be defined for using this method!") 3308 raise Exception("Account ID required") 3309 3310 if orderIDs: 3311 if allOrdersIDs is None or not allOrdersIDs: 3312 rawOrders = self.RequestPendingOrders() 3313 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3314 3315 if allStopOrdersIDs is None or not allStopOrdersIDs: 3316 rawStopOrders = self.RequestStopOrders() 3317 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3318 3319 for orderID in orderIDs: 3320 idInPendingOrders = orderID in allOrdersIDs 3321 idInStopOrders = orderID in allStopOrdersIDs 3322 3323 if not (idInPendingOrders or idInStopOrders): 3324 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3325 continue 3326 3327 else: 3328 if idInPendingOrders: 3329 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3330 3331 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3332 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3333 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3334 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3335 3336 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3337 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3338 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3339 3340 else: 3341 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3342 3343 elif idInStopOrders: 3344 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3345 3346 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3347 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3348 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3349 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3350 3351 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3352 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3353 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3354 3355 else: 3356 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3357 3358 else: 3359 continue 3360 3361 def CloseAllOrders(self) -> None: 3362 """ 3363 Gets a list of open pending and stop orders and cancel it all. 3364 """ 3365 rawOrders = self.RequestPendingOrders() 3366 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3367 lenOrders = len(allOrdersIDs) 3368 3369 rawStopOrders = self.RequestStopOrders() 3370 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3371 lenSOrders = len(allStopOrdersIDs) 3372 3373 if lenOrders > 0 or lenSOrders > 0: 3374 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3375 3376 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3377 3378 else: 3379 uLogger.info("Orders not found, nothing to cancel.") 3380 3381 def CloseAll(self, *args) -> None: 3382 """ 3383 Close all available (not blocked) opened trades and orders. 3384 3385 Also, you can select one or more keywords case-insensitive: 3386 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3387 3388 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3389 """ 3390 overview = self.Overview(show=False) # get all open trades info 3391 3392 if len(args) == 0: 3393 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3394 self.CloseAllOrders() # close all pending and stop orders 3395 3396 for iType in TKS_INSTRUMENTS: 3397 if iType != "Currencies": 3398 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3399 3400 else: 3401 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3402 lowerArgs = [x.lower() for x in args] 3403 3404 if "orders" in lowerArgs: 3405 self.CloseAllOrders() # close all pending and stop orders 3406 3407 for iType in TKS_INSTRUMENTS: 3408 if iType.lower() in lowerArgs and iType != "Currencies": 3409 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3410 3411 @staticmethod 3412 def ParseOrderParameters(operation, **inputParameters): 3413 """ 3414 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3415 3416 :param operation: string "Buy" or "Sell". 3417 :param inputParameters: this is dict of strings that looks like this 3418 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3419 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3420 "prices" key: one or more prices to open limit-orders 3421 Counts of values in lots and prices lists must be equals! 3422 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3423 """ 3424 # TODO: update order grid work with api v2 3425 pass 3426 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3427 # 3428 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3429 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3430 # raise Exception("Incorrect value") 3431 # 3432 # if "l" in inputParameters.keys(): 3433 # inputParameters["lots"] = inputParameters.pop("l") 3434 # 3435 # if "p" in inputParameters.keys(): 3436 # inputParameters["prices"] = inputParameters.pop("p") 3437 # 3438 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3439 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3440 # raise Exception("Incorrect value") 3441 # 3442 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3443 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3444 # 3445 # if len(lots) != len(prices): 3446 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3447 # raise Exception("Incorrect value") 3448 # 3449 # uLogger.debug("Extracted parameters for orders:") 3450 # uLogger.debug("lots = {}".format(lots)) 3451 # uLogger.debug("prices = {}".format(prices)) 3452 # 3453 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3454 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3455 # uLogger.debug("Order parameters: {}".format(result)) 3456 # 3457 # return result 3458 3459 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3460 """ 3461 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3462 3463 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3464 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3465 """ 3466 result = False 3467 msg = "Instrument not defined!" 3468 3469 if portfolio is None or not portfolio: 3470 portfolio = self.Overview(show=False) 3471 3472 if self.ticker: 3473 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3474 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3475 3476 for iType in TKS_INSTRUMENTS: 3477 for instrument in portfolio["stat"][iType]: 3478 if instrument["ticker"] == self.ticker: 3479 result = True 3480 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3481 break 3482 3483 elif self.figi: 3484 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3485 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3486 3487 for iType in TKS_INSTRUMENTS: 3488 for instrument in portfolio["stat"][iType]: 3489 if instrument["figi"] == self.figi: 3490 result = True 3491 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3492 break 3493 3494 else: 3495 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3496 3497 uLogger.debug(msg) 3498 3499 return result 3500 3501 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3502 """ 3503 Returns instrument is in the user's portfolio if it presents there. 3504 Instrument must be defined by `ticker` (highly priority) or `figi`. 3505 3506 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3507 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3508 """ 3509 result = None 3510 msg = "Instrument not defined!" 3511 3512 if portfolio is None or not portfolio: 3513 portfolio = self.Overview(show=False) 3514 3515 if self.ticker: 3516 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3517 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3518 3519 for iType in TKS_INSTRUMENTS: 3520 for instrument in portfolio["stat"][iType]: 3521 if instrument["ticker"] == self.ticker: 3522 result = instrument 3523 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3524 break 3525 3526 elif self.figi: 3527 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3528 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3529 3530 for iType in TKS_INSTRUMENTS: 3531 for instrument in portfolio["stat"][iType]: 3532 if instrument["figi"] == self.figi: 3533 result = instrument 3534 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3535 break 3536 3537 else: 3538 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3539 3540 uLogger.debug(msg) 3541 3542 return result 3543 3544 def RequestLimits(self) -> dict: 3545 """ 3546 Method for obtaining the available funds for withdrawal for current `accountId`. 3547 3548 See also: 3549 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3550 - `OverviewLimits()` method 3551 3552 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3553 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3554 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3555 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3556 """ 3557 if self.accountId is None or not self.accountId: 3558 uLogger.error("Variable `accountId` must be defined for using this method!") 3559 raise Exception("Account ID required") 3560 3561 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3562 3563 self.body = str({"accountId": self.accountId}) 3564 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3565 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3566 3567 uLogger.debug("Records about available funds for withdrawal successfully received") 3568 3569 return rawLimits 3570 3571 def OverviewLimits(self, show: bool = False) -> dict: 3572 """ 3573 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3574 3575 See also: `RequestLimits()`. 3576 3577 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3578 :return: dict with raw parsed data from server and some calculated statistics about it. 3579 """ 3580 if self.accountId is None or not self.accountId: 3581 uLogger.error("Variable `accountId` must be defined for using this method!") 3582 raise Exception("Account ID required") 3583 3584 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3585 3586 view = { 3587 "rawLimits": rawLimits, 3588 "limits": { # parsed data for every currency: 3589 "money": { # this is an array of portfolio currency positions 3590 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3591 }, 3592 "blocked": { # this is an array of blocked currency 3593 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3594 }, 3595 "blockedGuarantee": { # this is locked money under collateral for futures 3596 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3597 }, 3598 }, 3599 } 3600 3601 # --- Prepare text table with limits in human-readable format: 3602 if show: 3603 info = [ 3604 "# Withdrawal limits\n\n", 3605 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3606 "* **Account ID:** [{}]\n".format(self.accountId), 3607 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3608 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3609 ] 3610 3611 for curr in view["limits"]["money"].keys(): 3612 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3613 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3614 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3615 3616 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3617 "[{}]".format(curr), 3618 "{:.2f}".format(view["limits"]["money"][curr]), 3619 "{:.2f}".format(availableMoney), 3620 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3621 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3622 ) 3623 3624 if curr == "rub": 3625 info.insert(5, infoStr) # insert at first position in table and after headers 3626 3627 else: 3628 info.append(infoStr) 3629 3630 infoText = "".join(info) 3631 3632 uLogger.info(infoText) 3633 3634 if self.withdrawalLimitsFile: 3635 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3636 fH.write(infoText) 3637 3638 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3639 3640 return view 3641 3642 def RequestAccounts(self) -> dict: 3643 """ 3644 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3645 3646 See also: 3647 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3648 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3649 - `OverviewUserInfo()` method 3650 3651 :return: dict with raw data from server that contains accounts info. Example of dict: 3652 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3653 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3654 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3655 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3656 """ 3657 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3658 3659 self.body = str({}) 3660 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3661 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3662 3663 uLogger.debug("Records about available accounts successfully received") 3664 3665 return rawAccounts 3666 3667 def RequestUserInfo(self) -> dict: 3668 """ 3669 Method for requesting common user's information. 3670 3671 See also: 3672 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3673 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3674 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3675 - `OverviewUserInfo()` method 3676 3677 :return: dict with raw data from server that contains user's information. Example of dict: 3678 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3679 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3680 """ 3681 uLogger.debug("Requesting common user's information. Wait, please...") 3682 3683 self.body = str({}) 3684 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3685 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3686 3687 uLogger.debug("Records about current user successfully received") 3688 3689 return rawUserInfo 3690 3691 def RequestMarginStatus(self, accountId: str = None) -> dict: 3692 """ 3693 Method for requesting margin calculation for defined account ID. 3694 3695 See also: 3696 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3697 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3698 - `OverviewUserInfo()` method 3699 3700 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3701 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3702 Example of responses: 3703 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3704 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3705 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3706 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3707 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3708 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3709 """ 3710 if accountId is None or not accountId: 3711 if self.accountId is None or not self.accountId: 3712 uLogger.error("Variable `accountId` must be defined for using this method!") 3713 raise Exception("Account ID required") 3714 3715 else: 3716 accountId = self.accountId # use `self.accountId` (main ID) by default 3717 3718 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3719 3720 self.body = str({"accountId": accountId}) 3721 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3722 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3723 3724 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3725 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3726 rawMargin = {} 3727 3728 else: 3729 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3730 3731 return rawMargin 3732 3733 def RequestTariffLimits(self) -> dict: 3734 """ 3735 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3736 3737 See also: 3738 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3739 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3740 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3741 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3742 - `OverviewUserInfo()` method 3743 3744 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3745 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3746 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3747 """ 3748 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3749 3750 self.body = str({}) 3751 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3752 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3753 3754 uLogger.debug("Records with limits of current tariff successfully received") 3755 3756 return rawTariffLimits 3757 3758 def RequestBondCoupons(self, iJSON: dict) -> dict: 3759 """ 3760 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3761 then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". 3762 All dates are in UTC timezone. 3763 3764 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3765 Documentation: 3766 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3767 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3768 3769 See also: `ExtendBondsData()`. 3770 3771 :param iJSON: raw json data of a bond from broker server, example: `iJSON = self.iList["Bonds"][self.ticker]` 3772 If raw iJSON is not data of bond then server returns an error [400] with message: 3773 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3774 :return: dictionary with bond payment calendar. Response example: 3775 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3776 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3777 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3778 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3779 """ 3780 if iJSON["figi"] is None or not iJSON["figi"]: 3781 uLogger.error("FIGI must be defined for using this method!") 3782 raise Exception("FIGI required") 3783 3784 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3785 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3786 3787 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3788 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3789 self.figi, 3790 startDate, 3791 endDate, 3792 )) 3793 3794 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3795 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3796 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3797 3798 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3799 uLogger.warning("Instrument type is not bond!") 3800 3801 else: 3802 uLogger.debug("Records about bond payment calendar successfully received") 3803 3804 return calendar 3805 3806 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3807 """ 3808 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3809 pandas dataframe with more information about bonds: main info, current prices, bond payment calendar, 3810 coupon yields, current yields and some statistics etc. 3811 3812 WARNING! This is too long operation if a lot of bonds requested from broker server. 3813 3814 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3815 3816 :param instruments: list of strings with tickers or FIGIs. 3817 :param xlsx: if True then also exports pandas dataframe to xlsx-file `bondsXLSXFile`, default: `ext-bonds.xlsx`, 3818 for further used by data scientists or stock analytics. 3819 :return: wider pandas dataframe with more full and calculated data about bonds, than raw response from broker. 3820 In XLSX-file and pandas dataframe fields mean: 3821 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3822 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3823 """ 3824 if instruments is None or not instruments: 3825 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3826 raise Exception("Ticker or FIGI required") 3827 3828 if isinstance(instruments, str): 3829 instruments = [instruments] 3830 3831 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3832 3833 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3834 3835 iCount = len(uniqueInstruments) 3836 tooLong = iCount >= 20 3837 if tooLong: 3838 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3839 3840 bonds = None 3841 for i, self.figi in enumerate(uniqueInstruments): 3842 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3843 3844 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3845 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3846 rawBond = self.SearchByFIGI(requestPrice=True) 3847 3848 # Widen raw data with UTC current time (iData["actualDateTime"]): 3849 actualDate = datetime.now(tzutc()) 3850 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3851 3852 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3853 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3854 3855 # Replace some values with human-readable: 3856 iData["nominalCurrency"] = iData["nominal"]["currency"] 3857 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3858 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3859 iData["aciCurrency"] = iData["aciValue"]["currency"] 3860 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3861 iData["issueSize"] = int(iData["issueSize"]) 3862 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3863 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3864 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3865 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3866 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3867 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3868 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3869 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3870 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3871 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3872 3873 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3874 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3875 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3876 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3877 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3878 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3879 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3880 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3881 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3882 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3883 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3884 3885 # Widen raw data with calendar data from `rawCalendar` values: 3886 calendarData = [] 3887 for item in iData["rawCalendar"]["events"]: 3888 calendarData.append({ 3889 "couponDate": item["couponDate"], 3890 "couponNumber": int(item["couponNumber"]), 3891 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3892 "payCurrency": item["payOneBond"]["currency"], 3893 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3894 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3895 "couponStartDate": item["couponStartDate"], 3896 "couponEndDate": item["couponEndDate"], 3897 "couponPeriod": item["couponPeriod"], 3898 }) 3899 3900 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3901 if "maturityDate" not in iData.keys(): 3902 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3903 3904 # Widen raw data with Coupon Rate. 3905 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3906 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3907 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3908 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3909 3910 # Widen raw data with Yield to Maturity (YTM) on current date. 3911 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3912 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3913 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3914 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3915 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3916 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3917 3918 iData["calendar"] = calendarData # adds calendar at the end 3919 3920 # Remove not used data: 3921 iData.pop("uid") 3922 iData.pop("positionUid") 3923 iData.pop("currentPrice") 3924 iData.pop("rawCalendar") 3925 3926 colNames = list(iData.keys()) 3927 if bonds is None: 3928 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3929 3930 else: 3931 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3932 3933 else: 3934 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3935 3936 processed = round(100 * (i + 1) / iCount, 1) 3937 if tooLong and processed % 5 == 0: 3938 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3939 3940 else: 3941 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3942 3943 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3944 3945 # Saving bonds from pandas dataframe to XLSX sheet: 3946 if xlsx and self.bondsXLSXFile: 3947 with pd.ExcelWriter( 3948 path=self.bondsXLSXFile, 3949 date_format=TKS_DATE_FORMAT, 3950 datetime_format=TKS_DATE_TIME_FORMAT, 3951 mode="w", 3952 ) as writer: 3953 bonds.to_excel( 3954 writer, 3955 sheet_name="Extended bonds data", 3956 index=True, 3957 encoding="UTF-8", 3958 freeze_panes=(1, 1), 3959 ) # saving as XLSX-file with freeze first row and column as headers 3960 3961 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3962 3963 return bonds 3964 3965 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 3966 """ 3967 Creates bond payments calendar as pandas dataframe, and also save it to the XLSX-file, `calendar.xlsx` by default. 3968 3969 WARNING! This is too long operation if a lot of bonds requested from broker server. 3970 3971 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 3972 3973 :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains 3974 extended information about bonds: main info, current prices, bond payment calendar, 3975 coupon yields, current yields and some statistics etc. 3976 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 3977 :param xlsx: if True then also exports pandas dataframe to file `calendarFile` + `".xlsx"`, default: `calendar.xlsx`, 3978 for further used by data scientists or stock analytics. 3979 :return: pandas dataframe with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 3980 """ 3981 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 3982 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 3983 3984 uLogger.debug("Generating bond payments calendar data. Wait, please...") 3985 3986 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 3987 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 3988 calendar = None 3989 for bond in extBonds.iterrows(): 3990 for item in bond[1]["calendar"]: 3991 cData = { 3992 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 3993 "couponDate": item["couponDate"], 3994 "figi": bond[1]["figi"], 3995 "ticker": bond[1]["ticker"], 3996 "name": bond[1]["name"], 3997 "couponNumber": item["couponNumber"], 3998 "payOneBond": item["payOneBond"], 3999 "payCurrency": item["payCurrency"], 4000 "couponType": item["couponType"], 4001 "couponPeriod": item["couponPeriod"], 4002 "fixDate": item["fixDate"], 4003 "couponStartDate": item["couponStartDate"], 4004 "couponEndDate": item["couponEndDate"], 4005 } 4006 4007 if calendar is None: 4008 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4009 4010 else: 4011 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4012 4013 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4014 4015 # Saving calendar from pandas dataframe to XLSX sheet: 4016 if xlsx: 4017 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4018 4019 with pd.ExcelWriter( 4020 path=xlsxCalendarFile, 4021 date_format=TKS_DATE_FORMAT, 4022 datetime_format=TKS_DATE_TIME_FORMAT, 4023 mode="w", 4024 ) as writer: 4025 humanReadable = calendar.copy(deep=True) 4026 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4027 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4028 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4029 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4030 humanReadable.columns = colNames # human-readable column names 4031 4032 humanReadable.to_excel( 4033 writer, 4034 sheet_name="Bond payments calendar", 4035 index=False, 4036 encoding="UTF-8", 4037 freeze_panes=(1, 2), 4038 ) # saving as XLSX-file with freeze first row and column as headers 4039 4040 del humanReadable # release df in memory 4041 4042 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4043 4044 return calendar 4045 4046 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4047 """ 4048 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4049 Also, creates Markdown file with calendar data, `calendar.md` by default. 4050 4051 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4052 4053 :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains 4054 extended information about bonds: main info, current prices, bond payment calendar, 4055 coupon yields, current yields and some statistics etc. 4056 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4057 :param show: if `True` then also printing bonds payment calendar to the console, 4058 otherwise save to file `calendarFile` only. `False` by default. 4059 :return: multilines text in Markdown format with bonds payment calendar as a table. 4060 """ 4061 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4062 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4063 4064 infoText = "# Bond payments calendar\n\n" 4065 4066 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate pandas dataframe with full calendar data 4067 4068 if not calendar.empty: 4069 splitLine = "| | | | | | | | | |\n" 4070 4071 info = [ 4072 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4073 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4074 ] 4075 4076 newMonth = False 4077 notOneBond = calendar["figi"].nunique() > 1 4078 for i, bond in enumerate(calendar.iterrows()): 4079 if newMonth and notOneBond: 4080 info.append(splitLine) 4081 4082 info.append( 4083 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4084 " +" if bond[1]["paid"] else " —", 4085 bond[1]["couponDate"].split("T")[0], 4086 bond[1]["figi"], 4087 bond[1]["ticker"], 4088 bond[1]["couponNumber"], 4089 "{} {}".format( 4090 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4091 bond[1]["payCurrency"], 4092 ), 4093 bond[1]["couponType"], 4094 bond[1]["couponPeriod"], 4095 bond[1]["fixDate"].split("T")[0], 4096 ) 4097 ) 4098 4099 if i < len(calendar.values) - 1: 4100 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4101 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4102 newMonth = False if curDate.month == nextDate.month else True 4103 4104 else: 4105 newMonth = False 4106 4107 infoText += "".join(info) 4108 4109 if show: 4110 uLogger.info("{}".format(infoText)) 4111 4112 if self.calendarFile is not None: 4113 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4114 fH.write(infoText) 4115 4116 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4117 4118 else: 4119 infoText += "No data\n" 4120 4121 return infoText 4122 4123 def OverviewAccounts(self, show: bool = False) -> dict: 4124 """ 4125 Method for parsing and show simple table with all available user accounts. 4126 4127 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4128 4129 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4130 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4131 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4132 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4133 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4134 "closed": "—", "access": "Full access" }, ...}}` 4135 """ 4136 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4137 4138 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4139 accounts = { 4140 item["id"]: { 4141 "type": TKS_ACCOUNT_TYPES[item["type"]], 4142 "name": item["name"], 4143 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4144 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4145 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4146 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4147 } for item in rawAccounts["accounts"] 4148 } 4149 4150 # Raw and parsed data with some fields replaced in "stat" section: 4151 view = { 4152 "rawAccounts": rawAccounts, 4153 "stat": accounts, 4154 } 4155 4156 # --- Prepare simple text table with only accounts data in human-readable format: 4157 if show: 4158 info = [ 4159 "# User accounts\n\n", 4160 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4161 "| Account ID | Type | Status | Name |\n", 4162 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4163 ] 4164 4165 for account in view["stat"].keys(): 4166 info.extend([ 4167 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4168 account, 4169 view["stat"][account]["type"], 4170 view["stat"][account]["status"], 4171 view["stat"][account]["name"], 4172 ) 4173 ]) 4174 4175 infoText = "".join(info) 4176 4177 uLogger.info(infoText) 4178 4179 if self.userAccountsFile: 4180 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4181 fH.write(infoText) 4182 4183 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4184 4185 return view 4186 4187 def OverviewUserInfo(self, show: bool = False) -> dict: 4188 """ 4189 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4190 4191 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4192 4193 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4194 :return: dict with raw parsed data from server and some calculated statistics about it. 4195 """ 4196 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4197 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4198 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4199 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4200 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4201 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4202 4203 # This is dict with parsed common user data: 4204 userInfo = { 4205 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4206 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4207 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4208 "tariff": rawUserInfo["tariff"], 4209 } 4210 4211 # This is an array of dict with parsed margin statuses for every account IDs: 4212 margins = {} 4213 for accountId in accounts.keys(): 4214 if rawMargins[accountId]: 4215 margins[accountId] = { 4216 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4217 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4218 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4219 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4220 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4221 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4222 } 4223 4224 else: 4225 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4226 4227 unary = {} # unary-connection limits 4228 for item in rawTariffLimits["unaryLimits"]: 4229 if item["limitPerMinute"] in unary.keys(): 4230 unary[item["limitPerMinute"]].extend(item["methods"]) 4231 4232 else: 4233 unary[item["limitPerMinute"]] = item["methods"] 4234 4235 stream = {} # stream-connection limits 4236 for item in rawTariffLimits["streamLimits"]: 4237 if item["limit"] in stream.keys(): 4238 stream[item["limit"]].extend(item["streams"]) 4239 4240 else: 4241 stream[item["limit"]] = item["streams"] 4242 4243 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4244 limits = { 4245 "unary": unary, 4246 "stream": stream, 4247 } 4248 4249 # Raw and parsed data as an output result: 4250 view = { 4251 "rawUserInfo": rawUserInfo, 4252 "rawAccounts": rawAccounts, 4253 "rawMargins": rawMargins, 4254 "rawTariffLimits": rawTariffLimits, 4255 "stat": { 4256 "userInfo": userInfo, 4257 "accounts": accounts, 4258 "margins": margins, 4259 "limits": limits, 4260 }, 4261 } 4262 4263 # --- Prepare text table with user information in human-readable format: 4264 if show: 4265 info = [ 4266 "# Full user information\n\n", 4267 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4268 "## Common information\n\n", 4269 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4270 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4271 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4272 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4273 "\n## User accounts\n\n", 4274 ] 4275 4276 for account in view["stat"]["accounts"].keys(): 4277 info.extend([ 4278 "### ID: [{}]\n\n".format(account), 4279 "| Parameters | Values |\n", 4280 "|----------------------|--------------------------------------------------------------|\n", 4281 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4282 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4283 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4284 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4285 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4286 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4287 ]) 4288 4289 if margins[account]: 4290 info.extend([ 4291 "| Margin status: | Enabled |\n", 4292 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4293 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4294 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4295 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4296 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4297 ]) 4298 4299 else: 4300 info.append("| Margin status: | Disabled |\n\n") 4301 4302 info.extend([ 4303 "\n## Current user tariff limits\n", 4304 "\nSee also:\n", 4305 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4306 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4307 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4308 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4309 "\n### Unary limits\n", 4310 ]) 4311 4312 if unary: 4313 for key, values in sorted(unary.items()): 4314 info.append("\n* Max requests per minute: {}\n".format(key)) 4315 4316 for value in values: 4317 info.append(" - {}\n".format(value)) 4318 4319 else: 4320 info.append("\nNot available\n") 4321 4322 info.append("\n### Stream limits\n") 4323 4324 if stream: 4325 for key, values in sorted(stream.items()): 4326 info.append("\n* Max stream connections: {}\n".format(key)) 4327 4328 for value in values: 4329 info.append(" - {}\n".format(value)) 4330 4331 else: 4332 info.append("\nNot available\n") 4333 4334 infoText = "".join(info) 4335 4336 uLogger.info(infoText) 4337 4338 if self.userInfoFile: 4339 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4340 fH.write(infoText) 4341 4342 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4343 4344 return view 4345 4346 4347class Args: 4348 """ 4349 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4350 """ 4351 def __init__(self, **kwargs): 4352 self.__dict__.update(kwargs) 4353 4354 def __getattr__(self, item): 4355 return None 4356 4357 4358def ParseArgs(): 4359 """ 4360 Function get and parse command line keys. See examples: https://tim55667757.github.io/TKSBrokerAPI/ 4361 """ 4362 parser = ArgumentParser() # command-line string parser 4363 4364 parser.description = "TKSBrokerAPI is a python API to work with some methods of Tinkoff Open API using REST protocol. It can view history, orders and market information. Also, you can open orders and trades. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples" 4365 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4366 4367 # --- options: 4368 4369 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the program. `False` by default.") 4370 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4371 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4372 4373 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4374 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4375 4376 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4377 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4378 4379 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4380 4381 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4382 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4383 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4384 4385 parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4386 4387 # --- commands: 4388 4389 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4390 4391 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4392 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4393 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider pandas dataframe with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4394 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4395 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4396 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4397 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4398 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4399 4400 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4401 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4402 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4403 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4404 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4405 4406 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4407 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4408 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4409 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4410 4411 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4412 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4413 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4414 4415 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4416 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4417 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4418 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4419 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4420 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4421 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4422 4423 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4424 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4425 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.") 4426 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.") 4427 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4428 4429 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4430 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4431 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4432 4433 cmdArgs = parser.parse_args() 4434 return cmdArgs 4435 4436 4437def Main(**kwargs): 4438 """ 4439 Main function for work with Tinkoff Open API service. It realizes simple logic: get a lot of options and execute one command. 4440 4441 See examples: https://tim55667757.github.io/TKSBrokerAPI/ 4442 """ 4443 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4444 4445 if args.debug_level: 4446 uLogger.level = 10 # always debug level by default 4447 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4448 4449 exitCode = 0 4450 start = datetime.now(tzutc()) 4451 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4452 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4453 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4454 )) 4455 4456 # trying to calculate full current version: 4457 buildVersion = __version__ 4458 try: 4459 v = version("tksbrokerapi") 4460 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4461 4462 except Exception: 4463 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4464 4465 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4466 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4467 4468 try: 4469 if args.version: 4470 print("TKSBrokerAPI {}".format(buildVersion)) 4471 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4472 4473 else: 4474 # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader` 4475 server = TinkoffBrokerServer( 4476 token=args.token, 4477 accountId=args.account_id, 4478 useCache=not args.no_cache, 4479 ) 4480 4481 # --- set some options: 4482 4483 if args.ticker: 4484 if args.ticker in server.aliasesKeys: 4485 server.ticker = server.aliases[args.ticker] # Replace some tickers with its aliases 4486 4487 else: 4488 server.ticker = args.ticker 4489 4490 if args.figi: 4491 server.figi = args.figi 4492 4493 if args.depth is not None: 4494 server.depth = args.depth 4495 4496 # --- do one of commands: 4497 4498 if args.list: 4499 if args.output is not None: 4500 server.instrumentsFile = args.output 4501 4502 server.ShowInstrumentsInfo(show=True) 4503 4504 elif args.list_xlsx: 4505 server.DumpInstrumentsAsXLSX(forceUpdate=False) 4506 4507 elif args.bonds_xlsx is not None: 4508 if args.output is not None: 4509 server.bondsXLSXFile = args.output 4510 4511 if len(args.bonds_xlsx) == 0: 4512 server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4513 4514 else: 4515 server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4516 4517 elif args.search: 4518 if args.output is not None: 4519 server.searchResultsFile = args.output 4520 4521 server.SearchInstruments(pattern=args.search[0], show=True) 4522 4523 elif args.info: 4524 if not (args.ticker or args.figi): 4525 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4526 raise Exception("Ticker or FIGI required") 4527 4528 if args.output is not None: 4529 server.infoFile = args.output 4530 4531 if args.ticker: 4532 server.SearchByTicker(requestPrice=True, show=True, debug=False) # show info and current prices by ticker name 4533 4534 else: 4535 server.SearchByFIGI(requestPrice=True, show=True, debug=False) # show info and current prices by FIGI id 4536 4537 elif args.calendar is not None: 4538 if args.output is not None: 4539 server.calendarFile = args.output 4540 4541 if len(args.calendar) == 0: 4542 bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4543 4544 else: 4545 bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4546 4547 server.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4548 4549 elif args.price: 4550 if not (args.ticker or args.figi): 4551 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4552 raise Exception("Ticker or FIGI required") 4553 4554 server.GetCurrentPrices(show=True) 4555 4556 elif args.prices is not None: 4557 if args.output is not None: 4558 server.pricesFile = args.output 4559 4560 server.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4561 4562 elif args.overview: 4563 if args.output is not None: 4564 server.overviewFile = args.output 4565 4566 server.Overview(show=True, details="full") 4567 4568 elif args.overview_digest: 4569 if args.output is not None: 4570 server.overviewDigestFile = args.output 4571 4572 server.Overview(show=True, details="digest") 4573 4574 elif args.overview_positions: 4575 if args.output is not None: 4576 server.overviewPositionsFile = args.output 4577 4578 server.Overview(show=True, details="positions") 4579 4580 elif args.overview_orders: 4581 if args.output is not None: 4582 server.overviewOrdersFile = args.output 4583 4584 server.Overview(show=True, details="orders") 4585 4586 elif args.overview_analytics: 4587 if args.output is not None: 4588 server.overviewAnalyticsFile = args.output 4589 4590 server.Overview(show=True, details="analytics") 4591 4592 elif args.deals is not None: 4593 if args.output is not None: 4594 server.reportFile = args.output 4595 4596 if 0 <= len(args.deals) < 3: 4597 server.Deals( 4598 start=args.deals[0] if len(args.deals) >= 1 else None, 4599 end=args.deals[1] if len(args.deals) == 2 else None, 4600 show=True, # Always show deals report in console 4601 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4602 ) 4603 4604 else: 4605 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4606 raise Exception("Incorrect value") 4607 4608 elif args.history is not None: 4609 if args.output is not None: 4610 server.historyFile = args.output 4611 4612 if 0 <= len(args.history) < 3: 4613 dataReceived = server.History( 4614 start=args.history[0] if len(args.history) >= 1 else None, 4615 end=args.history[1] if len(args.history) == 2 else None, 4616 interval="hour" if args.interval is None or not args.interval else args.interval, 4617 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4618 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4619 show=True, # shows all downloaded candles in console 4620 ) 4621 4622 if args.render_chart is not None and dataReceived is not None: 4623 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4624 4625 server.ShowHistoryChart( 4626 candles=dataReceived, 4627 interact=iChart, 4628 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4629 ) 4630 4631 else: 4632 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4633 raise Exception("Incorrect value") 4634 4635 elif args.load_history is not None: 4636 histData = server.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4637 4638 if args.render_chart is not None and histData is not None: 4639 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4640 server.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4641 4642 server.ShowHistoryChart( 4643 candles=histData, 4644 interact=iChart, 4645 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4646 ) 4647 4648 elif args.trade is not None: 4649 if 1 <= len(args.trade) <= 5: 4650 server.Trade( 4651 operation=args.trade[0], 4652 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4653 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4654 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4655 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4656 ) 4657 4658 else: 4659 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4660 4661 elif args.buy is not None: 4662 if 0 <= len(args.buy) <= 4: 4663 server.Buy( 4664 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4665 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4666 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4667 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4668 ) 4669 4670 else: 4671 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4672 4673 elif args.sell is not None: 4674 if 0 <= len(args.sell) <= 4: 4675 server.Sell( 4676 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4677 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4678 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4679 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4680 ) 4681 4682 else: 4683 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4684 4685 elif args.order: 4686 if 4 <= len(args.order) <= 7: 4687 server.Order( 4688 operation=args.order[0], 4689 orderType=args.order[1], 4690 lots=int(args.order[2]), 4691 targetPrice=float(args.order[3]), 4692 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4693 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4694 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4695 ) 4696 4697 else: 4698 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4699 4700 elif args.buy_limit: 4701 server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4702 4703 elif args.sell_limit: 4704 server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4705 4706 elif args.buy_stop: 4707 if 2 <= len(args.buy_stop) <= 7: 4708 server.BuyStop( 4709 lots=int(args.buy_stop[0]), 4710 targetPrice=float(args.buy_stop[1]), 4711 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4712 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4713 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4714 ) 4715 4716 else: 4717 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4718 4719 elif args.sell_stop: 4720 if 2 <= len(args.sell_stop) <= 7: 4721 server.SellStop( 4722 lots=int(args.sell_stop[0]), 4723 targetPrice=float(args.sell_stop[1]), 4724 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4725 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4726 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4727 ) 4728 4729 else: 4730 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4731 4732 # elif args.buy_order_grid is not None: 4733 # # update order grid work with api v2 4734 # if len(args.buy_order_grid) == 2: 4735 # orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4736 # 4737 # for order in orderParams: 4738 # server.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4739 # 4740 # else: 4741 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4742 # 4743 # elif args.sell_order_grid is not None: 4744 # # update order grid work with api v2 4745 # if len(args.sell_order_grid) >= 2: 4746 # orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4747 # 4748 # for order in orderParams: 4749 # server.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4750 # 4751 # else: 4752 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4753 4754 elif args.close_order is not None: 4755 server.CloseOrders(args.close_order) # close only one order 4756 4757 elif args.close_orders is not None: 4758 server.CloseOrders(args.close_orders) # close list of orders 4759 4760 elif args.close_trade: 4761 if not args.ticker: 4762 uLogger.error("`--ticker` key is required for this operation!") 4763 raise Exception("Ticker required") 4764 4765 server.CloseTrades([args.ticker]) # close only one trade 4766 4767 elif args.close_trades is not None: 4768 server.CloseTrades(args.close_trades) # close trades for list of tickers 4769 4770 elif args.close_all is not None: 4771 server.CloseAll(*args.close_all) 4772 4773 elif args.limits: 4774 if args.output is not None: 4775 server.withdrawalLimitsFile = args.output 4776 4777 server.OverviewLimits(show=True) 4778 4779 elif args.user_info: 4780 if args.output is not None: 4781 server.userInfoFile = args.output 4782 4783 server.OverviewUserInfo(show=True) 4784 4785 elif args.account: 4786 if args.output is not None: 4787 server.userAccountsFile = args.output 4788 4789 server.OverviewAccounts(show=True) 4790 4791 else: 4792 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4793 raise Exception("There is no command to execute") 4794 4795 except Exception: 4796 trace = tb.format_exc() 4797 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4798 if e in trace: 4799 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4800 break 4801 4802 uLogger.debug(trace) 4803 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4804 exitCode = 255 # an error occurred, must be open a ticket for this issue 4805 4806 finally: 4807 finish = datetime.now(tzutc()) 4808 4809 if exitCode == 0: 4810 uLogger.debug("All operations were finished success (summary code is 0).") 4811 4812 else: 4813 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4814 os.path.abspath(uLog.defaultLogFile), exitCode, 4815 )) 4816 4817 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4818 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4819 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4820 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4821 )) 4822 4823 if not kwargs: 4824 sys.exit(exitCode) 4825 4826 else: 4827 return exitCode 4828 4829 4830if __name__ == "__main__": 4831 Main()
78def NanoToFloat(units: str, nano: int) -> float: 79 """ 80 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 81 82 `NanoToFloat(units="2", nano=500000000) -> 2.5` 83 84 `NanoToFloat(units="0", nano=50000000) -> 0.05` 85 86 :param units: integer string or integer parameter that represents the integer part of number 87 :param nano: integer string or integer parameter that represents the fractional part of number 88 :return: float view of number 89 """ 90 return int(units) + int(nano) * NANO
Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:
NanoToFloat(units="2", nano=500000000) -> 2.5
NanoToFloat(units="0", nano=50000000) -> 0.05
Parameters
- units: integer string or integer parameter that represents the integer part of number
- nano: integer string or integer parameter that represents the fractional part of number
Returns
float view of number
93def FloatToNano(number: float) -> dict: 94 """ 95 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 96 97 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 98 99 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 100 101 :param number: float number 102 :return: nano-type view of number: `{"units": "string", "nano": integer}` 103 """ 104 splitByPoint = str(number).split(".") 105 frac = 0 106 107 if len(splitByPoint) > 1: 108 if len(splitByPoint[1]) <= 9: 109 frac = int("{}{}".format( 110 int(splitByPoint[1]), 111 "0" * (9 - len(splitByPoint[1])), 112 )) 113 114 if (number < 0) and (frac > 0): 115 frac = -frac 116 117 return {"units": str(int(number)), "nano": frac}
Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:
FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}
FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}
Parameters
- number: float number
Returns
nano-type view of number:
{"units": "string", "nano": integer}
120def GetDatesAsString(start: str = None, end: str = None) -> tuple: 121 """ 122 Create tuple of date and time strings with timezone parsed from user-friendly date. 123 124 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 125 126 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 127 An error exception will occur if input date has incorrect format. 128 129 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 130 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 131 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 132 Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago. 133 134 Also, you can use keywords for start if `end=None`: 135 `today` (from 00:00:00 to the end of current day), 136 `yesterday` (-1 day from 00:00:00 to 23:59:59), 137 `week` (-7 day from 00:00:00 to the end of current day), 138 `month` (-30 day from 00:00:00 to the end of current day), 139 `year` (-365 day from 00:00:00 to the end of current day), 140 141 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 142 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 143 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 144 """ 145 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 146 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 147 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 148 149 # time between start and the end of the current day: 150 if start is None or start.lower() == "today": 151 pass 152 153 # from start of the last day to the end of the last day: 154 elif start.lower() == "yesterday": 155 s -= timedelta(days=1) 156 e -= timedelta(days=1) 157 158 # week (-7 day from 00:00:00 to the end of the current day): 159 elif start.lower() == "week": 160 s -= timedelta(days=6) # +1 current day already taken into account 161 162 # month (-30 day from 00:00:00 to the end of current day): 163 elif start.lower() == "month": 164 s -= timedelta(days=29) # +1 current day already taken into account 165 166 # year (-365 day from 00:00:00 to the end of current day): 167 elif start.lower() == "year": 168 s -= timedelta(days=364) # +1 current day already taken into account 169 170 # -N days ago to the end of current day: 171 elif start.startswith('-') and start[1:].isdigit(): 172 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 173 174 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 175 else: 176 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 177 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 178 179 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 180 s = s.strftime(TKS_DATE_TIME_FORMAT) 181 e = e.strftime(TKS_DATE_TIME_FORMAT) 182 183 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 184 185 return s, e
Create tuple of date and time strings with timezone parsed from user-friendly date.
User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).
Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.
If start=None, end=None then return dates from yesterday to the end of the day.
If start=some_date_1, end=None then return dates from some_date_1 to the end of the day.
If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2.
Start day may be negative integer numbers: -1, -2, -3 - how many days ago.
Also, you can use keywords for start if end=None:
today (from 00:00:00 to the end of current day),
yesterday (-1 day from 00:00:00 to 23:59:59),
week (-7 day from 00:00:00 to the end of current day),
month (-30 day from 00:00:00 to the end of current day),
year (-365 day from 00:00:00 to the end of current day),
Returns
tuple with 2 strings
(start, end)dates in UTC ISO time format%Y-%m-%dT%H:%M:%SZfor OpenAPI. See date and time format here:TKSEnums.TKS_DATE_TIME_FORMAT. Example:("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.
188class TinkoffBrokerServer: 189 """ 190 This class implements methods to work with Tinkoff broker server. 191 192 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 193 194 About `token`: https://tinkoff.github.io/investAPI/token/ 195 """ 196 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 197 """ 198 Main class init. 199 200 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 201 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 202 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 203 :param useCache: use default cache file with raw data to use instead of `iList`. 204 True by default. Cache is auto-update if new day has come. 205 If you don't want to use cache and always updates raw data then set `useCache=False`. 206 :param defaultCache: path to default cache file. `dump.json` by default. 207 """ 208 if token is None or not token: 209 try: 210 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 211 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 212 213 except KeyError: 214 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 215 raise Exception("Token required") 216 217 else: 218 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 219 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 220 221 if accountId is None or not accountId: 222 try: 223 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 224 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 225 226 except KeyError: 227 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 228 229 else: 230 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 231 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 232 233 self.version = __version__ # duplicate here used TKSBrokerAPI main version 234 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 235 236 Latest version: https://pypi.org/project/tksbrokerapi/ 237 """ 238 239 self.aliases = TKS_TICKER_ALIASES 240 """Some aliases instead official tickers. 241 242 See also: `TKSEnums.TKS_TICKER_ALIASES` 243 """ 244 245 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 246 247 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 248 249 self.ticker = "" 250 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 251 252 See also: `SearchByTicker()`, `SearchInstruments()`. 253 """ 254 255 self.figi = "" 256 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 257 258 See also: `SearchByFIGI()`, `SearchInstruments()`. 259 """ 260 261 self.depth = 1 262 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 263 264 See also: `GetCurrentPrices()`. 265 """ 266 267 self.server = r"https://invest-public-api.tinkoff.ru/rest" 268 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 269 270 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 271 """ 272 273 uLogger.debug("Broker API server: {}".format(self.server)) 274 275 self.timeout = 15 276 """Server operations timeout in seconds. Default: `15`. 277 278 See also: `SendAPIRequest()`. 279 """ 280 281 self.headers = { 282 "Content-Type": "application/json", 283 "accept": "application/json", 284 "Authorization": "Bearer {}".format(self.token), 285 "x-app-name": "Tim55667757.TKSBrokerAPI", 286 } 287 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 288 289 See also: `SendAPIRequest()`. 290 """ 291 292 self.body = None 293 """Request body which send to broker server. Default: `None`. 294 295 See also: `SendAPIRequest()`. 296 """ 297 298 self.historyFile = None 299 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only pandas dataframe. 300 301 See also: `History()`. 302 """ 303 304 self.htmlHistoryFile = "index.html" 305 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 306 307 See also: `ShowHistoryChart()`. 308 """ 309 310 self.instrumentsFile = "instruments.md" 311 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 312 313 See also: `ShowInstrumentsInfo()`. 314 """ 315 316 self.searchResultsFile = "search-results.md" 317 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 318 319 See also: `SearchInstruments()`. 320 """ 321 322 self.pricesFile = "prices.md" 323 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 324 325 See also: `GetListOfPrices()`. 326 """ 327 328 self.infoFile = "info.md" 329 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 330 331 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 332 """ 333 334 self.bondsXLSXFile = "ext-bonds.xlsx" 335 """Filename where wider pandas dataframe with more information about bonds: main info, current prices, 336 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 337 338 See also: `ExtendBondsData()`. 339 """ 340 341 self.calendarFile = "calendar.md" 342 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 343 344 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 345 346 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 347 """ 348 349 self.overviewFile = "overview.md" 350 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 351 352 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 353 """ 354 355 self.overviewDigestFile = "overview-digest.md" 356 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 357 358 See also: `Overview()` with parameter `details="digest"`. 359 """ 360 361 self.overviewPositionsFile = "overview-positions.md" 362 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 363 364 See also: `Overview()` with parameter `details="positions"`. 365 """ 366 367 self.overviewOrdersFile = "overview-orders.md" 368 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 369 370 See also: `Overview()` with parameter `details="orders"`. 371 """ 372 373 self.overviewAnalyticsFile = "overview-analytics.md" 374 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 375 376 See also: `Overview()` with parameter `details="analytics"`. 377 """ 378 379 self.reportFile = "deals.md" 380 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 381 382 See also: `Deals()`. 383 """ 384 385 self.withdrawalLimitsFile = "limits.md" 386 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 387 388 See also: `OverviewLimits()` and `RequestLimits()`. 389 """ 390 391 self.userInfoFile = "user-info.md" 392 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 393 394 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 395 """ 396 397 self.userAccountsFile = "accounts.md" 398 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 399 400 See also: `OverviewAccounts()`, `RequestAccounts()`. 401 """ 402 403 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 404 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 405 406 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 407 408 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 409 """ 410 411 self.iList = None # init iList for raw instruments data 412 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 413 414 See also: `Listing()`, `DumpInstruments()`. 415 """ 416 417 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 418 if useCache: 419 if os.path.exists(self.iListDumpFile): 420 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 421 curTime = datetime.now(tzutc()) 422 423 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 424 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 425 426 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 427 428 else: 429 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 430 431 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 432 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 433 434 else: 435 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 436 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 437 438 else: 439 self.iList = self.Listing() # request new raw instruments data from broker server 440 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 441 442 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 443 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 444 445 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 446 """ 447 448 @staticmethod 449 def _ParseJSON(rawData="{}", debug: bool = False) -> dict: 450 """ 451 Parse JSON from response string. 452 453 :param rawData: this is a string with JSON-formatted text. 454 :param debug: if `True` then print more debug information. 455 :return: JSON (dictionary), parsed from server response string. 456 """ 457 if debug: 458 uLogger.debug("Raw text body:") 459 uLogger.debug(rawData) 460 461 responseJSON = json.loads(rawData) if rawData else {} 462 463 if debug: 464 uLogger.debug("JSON formatted:") 465 for jsonLine in json.dumps(responseJSON, indent=4).split('\n'): 466 uLogger.debug(jsonLine) 467 468 return responseJSON 469 470 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 471 """ 472 Send GET or POST request to broker server and receive JSON object. 473 474 self.header: must be defining with dictionary of headers. 475 self.body: if define then used as request body. None by default. 476 self.timeout: global request timeout, 15 seconds by default. 477 :param url: url with REST request. 478 :param reqType: send "GET" or "POST" request. "GET" by default. 479 :param retry: how many times retry after first request if an 5xx server errors occurred. 480 :param pause: sleep time in seconds between retries. 481 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 482 :return: response JSON (dictionary) from broker. 483 """ 484 if reqType not in ("GET", "POST"): 485 uLogger.error("You can define request type: 'GET' or 'POST'!") 486 raise Exception("Incorrect value") 487 488 if debug: 489 uLogger.debug("Request parameters:") 490 uLogger.debug(" - REST API URL: {}".format(url)) 491 uLogger.debug(" - request type: {}".format(reqType)) 492 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 493 uLogger.debug(" - body: {}".format(self.body)) 494 495 # fast hack to avoid all operations with some tickers/FIGI 496 responseJSON = {} 497 oK = True 498 for item in self.exclude: 499 if item in url: 500 if debug: 501 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 502 503 oK = False 504 break 505 506 if oK: 507 counter = 0 508 response = None 509 errMsg = "" 510 511 while not response and counter <= retry: 512 if reqType == "GET": 513 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 514 515 if reqType == "POST": 516 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 517 518 if debug: 519 uLogger.debug("Response:") 520 uLogger.debug(" - status code: {}".format(response.status_code)) 521 uLogger.debug(" - reason: {}".format(response.reason)) 522 uLogger.debug(" - body length: {}".format(len(response.text))) 523 uLogger.debug(" - headers: {}".format(response.headers)) 524 525 # Server returns some headers: 526 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 527 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 528 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 529 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 530 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 531 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 532 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 533 sleep(rateLimitWait) 534 535 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 536 if 400 <= response.status_code < 500: 537 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 538 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 539 counter = retry + 1 540 541 if 500 <= response.status_code < 600: 542 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 543 uLogger.debug(" - not oK, {}".format(errMsg)) 544 counter += 1 545 546 if counter <= retry: 547 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 548 sleep(pause) 549 550 responseJSON = self._ParseJSON(response.text) 551 552 if errMsg: 553 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 554 uLogger.error(" - not oK, {}".format(errMsg)) 555 556 return responseJSON 557 558 def _IUpdater(self, iType: str) -> tuple: 559 """ 560 Request instrument by type from server. See available API methods for instruments: 561 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 562 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 563 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 564 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 565 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 566 567 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 568 :return: tuple with iType name and list of available instruments of current type for defined user token. 569 """ 570 result = [] 571 572 if iType in TKS_INSTRUMENTS: 573 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 574 575 # all instruments have the same body in API v2 requests: 576 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 577 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 578 result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"] 579 580 return iType, result 581 582 def _IWrapper(self, kwargs): 583 """ 584 Wrapper runs instrument's update method `_IUpdater()`. 585 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 586 """ 587 return self._IUpdater(**kwargs) 588 589 def Listing(self) -> dict: 590 """ 591 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 592 593 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 594 """ 595 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 596 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 597 598 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 599 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 600 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 601 602 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 603 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 604 poolUpdater.close() 605 606 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 607 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 608 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 609 610 # calculate minimum price increment (step) for all instruments and set up instrument's type: 611 for iType in iList.keys(): 612 for ticker in iList[iType]: 613 iList[iType][ticker]["type"] = iType 614 615 if "minPriceIncrement" in iList[iType][ticker].keys(): 616 iList[iType][ticker]["step"] = NanoToFloat( 617 iList[iType][ticker]["minPriceIncrement"]["units"], 618 iList[iType][ticker]["minPriceIncrement"]["nano"], 619 ) 620 621 else: 622 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 623 624 return iList 625 626 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 627 """ 628 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 629 630 See also: `DumpInstruments()`, `Listing()`. 631 632 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 633 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 634 """ 635 if self.iListDumpFile is None or not self.iListDumpFile: 636 uLogger.error("Output name of dump file must be defined!") 637 raise Exception("Filename required") 638 639 if not self.iList or forceUpdate: 640 self.iList = self.Listing() 641 642 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 643 644 # Save as XLSX with separated sheets for every type of instruments: 645 with pd.ExcelWriter( 646 path=xlsxDumpFile, 647 date_format=TKS_DATE_FORMAT, 648 datetime_format=TKS_DATE_TIME_FORMAT, 649 mode="w", 650 ) as writer: 651 for iType in TKS_INSTRUMENTS: 652 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 653 df = df[sorted(df)] # sorted by column names 654 df = df.applymap( 655 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 656 na_action="ignore", 657 ) # converting numbers from nano-type to float in every cell 658 df.to_excel( 659 writer, 660 sheet_name=iType, 661 encoding="UTF-8", 662 freeze_panes=(1, 1), 663 ) # saving as XLSX-file with freeze first row and column as headers 664 665 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 666 667 def DumpInstruments(self, forceUpdate: bool = True) -> str: 668 """ 669 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 670 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 671 672 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 673 674 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 675 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 676 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 677 """ 678 if self.iListDumpFile is None or not self.iListDumpFile: 679 uLogger.error("Output name of dump file must be defined!") 680 raise Exception("Filename required") 681 682 if not self.iList or forceUpdate: 683 self.iList = self.Listing() 684 685 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 686 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 687 fH.write(jsonDump) 688 689 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 690 691 return jsonDump 692 693 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 694 """ 695 Show information about one instrument defined by json data and prints it in Markdown format. 696 697 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 698 699 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 700 :param show: if `True` then also printing information about instrument and its current price. 701 :return: multilines text in Markdown format with information about one instrument. 702 """ 703 splitLine = "| | |\n" 704 infoText = "" 705 706 if iJSON is not None and iJSON and isinstance(iJSON, dict): 707 info = [ 708 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 709 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 710 "| Parameters | Values |\n", 711 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 712 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 713 "| Full name: | {:<54} |\n".format(iJSON["name"]), 714 ] 715 716 if "sector" in iJSON.keys() and iJSON["sector"]: 717 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 718 719 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 720 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 721 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 722 ))) 723 724 info.extend([ 725 splitLine, 726 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 727 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 728 ]) 729 730 if "isin" in iJSON.keys() and iJSON["isin"]: 731 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 732 733 if "classCode" in iJSON.keys(): 734 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 735 736 info.extend([ 737 splitLine, 738 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 739 splitLine, 740 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 741 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 742 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 743 ]) 744 745 if iJSON["figi"]: 746 self.figi = iJSON["figi"] 747 iJSON = iJSON | self.RequestTradingStatus() 748 749 info.extend([ 750 splitLine, 751 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 752 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 753 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 754 ]) 755 756 info.append(splitLine) 757 758 if "type" in iJSON.keys() and iJSON["type"]: 759 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 760 761 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 762 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 763 764 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 765 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 766 767 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 768 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 769 770 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 771 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 772 773 if "focusType" in iJSON.keys() and iJSON["focusType"]: 774 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 775 776 if "assetType" in iJSON.keys() and iJSON["assetType"]: 777 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 778 779 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 780 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 781 782 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 783 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 784 785 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 786 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 787 788 if "currency" in iJSON.keys(): 789 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 790 791 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 792 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 793 794 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 795 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 796 797 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 798 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 799 800 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 801 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 802 803 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 804 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 805 806 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 807 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 808 809 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 810 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 811 812 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 813 info.append("| Perpetual bond: | Yes |\n") 814 815 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 816 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 817 818 iExt = None 819 if iJSON["type"] == "Bonds": 820 info.extend([ 821 splitLine, 822 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 823 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 824 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 825 iJSON["nominal"]["currency"], 826 )), 827 ]) 828 829 if "floatingCouponFlag" in iJSON.keys(): 830 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 831 832 if "amortizationFlag" in iJSON.keys(): 833 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 834 835 info.append(splitLine) 836 837 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 838 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 839 840 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 841 842 info.extend([ 843 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 844 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 845 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 846 ]) 847 848 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 849 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 850 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 851 iJSON["aciValue"]["currency"] 852 ))) 853 854 if "currentPrice" in iJSON.keys(): 855 info.append(splitLine) 856 857 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 858 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 859 860 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 861 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 862 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 863 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 864 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 865 866 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 867 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 868 869 info.extend([ 870 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 871 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 872 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 873 )), 874 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 875 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 876 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 877 )), 878 "| Changes between last deal price and last close | {:<54} |\n".format( 879 "{:.2f}%{}".format( 880 iJSON["currentPrice"]["changes"], 881 " ({}{:.2f} {})".format( 882 "+" if bondChangesDelta > 0 else "", 883 bondChangesDelta, 884 aciCurrency 885 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 886 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 887 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 888 currency 889 ), 890 ) 891 ), 892 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 893 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 894 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 895 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 896 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 897 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 898 )), 899 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 900 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 901 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 902 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 903 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 904 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 905 )), 906 ]) 907 908 if "lot" in iJSON.keys(): 909 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 910 911 if "step" in iJSON.keys() and iJSON["step"] != 0: 912 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 913 914 # Add bond payment calendar: 915 if iJSON["type"] == "Bonds": 916 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 917 info.extend(["\n", strCalendar]) 918 919 infoText += "".join(info) 920 921 if show: 922 uLogger.info("{}".format(infoText)) 923 924 else: 925 uLogger.debug("{}".format(infoText)) 926 927 if self.infoFile is not None: 928 with open(self.infoFile, "w", encoding="UTF-8") as fH: 929 fH.write(infoText) 930 931 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 932 933 return infoText 934 935 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 936 """ 937 Search and return raw broker's information about instrument by its ticker. 938 `ticker` must be defined! If debug=True then print all debug messages. 939 940 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 941 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 942 :param debug: if `True` then print all debug console messages. 943 :return: JSON formatted data with information about instrument. 944 """ 945 tickerJSON = {} 946 if debug: 947 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 948 949 if not self.ticker: 950 uLogger.warning("self.ticker variable is not be empty!") 951 952 else: 953 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 954 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 955 raise Exception("Instrument not allowed") 956 957 if not self.iList: 958 self.iList = self.Listing() 959 960 if self.ticker in self.iList["Shares"].keys(): 961 tickerJSON = self.iList["Shares"][self.ticker] 962 if debug: 963 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 964 965 elif self.ticker in self.iList["Currencies"].keys(): 966 tickerJSON = self.iList["Currencies"][self.ticker] 967 if debug: 968 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 969 970 elif self.ticker in self.iList["Bonds"].keys(): 971 tickerJSON = self.iList["Bonds"][self.ticker] 972 if debug: 973 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 974 975 elif self.ticker in self.iList["Etfs"].keys(): 976 tickerJSON = self.iList["Etfs"][self.ticker] 977 if debug: 978 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 979 980 elif self.ticker in self.iList["Futures"].keys(): 981 tickerJSON = self.iList["Futures"][self.ticker] 982 if debug: 983 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 984 985 if tickerJSON: 986 self.figi = tickerJSON["figi"] 987 988 if requestPrice: 989 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 990 991 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 992 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 993 994 else: 995 tickerJSON["currentPrice"]["changes"] = 0 996 997 if show: 998 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 999 1000 else: 1001 if show: 1002 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1003 1004 return tickerJSON 1005 1006 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1007 """ 1008 Search and return raw broker's information about instrument by its FIGI. 1009 `figi` must be defined! If debug=True then print all debug messages. 1010 1011 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1012 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1013 :param debug: if `True` then print all debug console messages. 1014 :return: JSON formatted data with information about instrument. 1015 """ 1016 figiJSON = {} 1017 if debug: 1018 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1019 1020 if not self.figi: 1021 uLogger.warning("self.figi variable is not be empty!") 1022 1023 else: 1024 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1025 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1026 raise Exception("Instrument not allowed") 1027 1028 if not self.iList: 1029 self.iList = self.Listing() 1030 1031 for item in self.iList["Shares"].keys(): 1032 if self.figi == self.iList["Shares"][item]["figi"]: 1033 figiJSON = self.iList["Shares"][item] 1034 1035 if debug: 1036 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1037 1038 break 1039 1040 if not figiJSON: 1041 for item in self.iList["Currencies"].keys(): 1042 if self.figi == self.iList["Currencies"][item]["figi"]: 1043 figiJSON = self.iList["Currencies"][item] 1044 1045 if debug: 1046 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1047 1048 break 1049 1050 if not figiJSON: 1051 for item in self.iList["Bonds"].keys(): 1052 if self.figi == self.iList["Bonds"][item]["figi"]: 1053 figiJSON = self.iList["Bonds"][item] 1054 1055 if debug: 1056 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1057 1058 break 1059 1060 if not figiJSON: 1061 for item in self.iList["Etfs"].keys(): 1062 if self.figi == self.iList["Etfs"][item]["figi"]: 1063 figiJSON = self.iList["Etfs"][item] 1064 1065 if debug: 1066 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1067 1068 break 1069 1070 if not figiJSON: 1071 for item in self.iList["Futures"].keys(): 1072 if self.figi == self.iList["Futures"][item]["figi"]: 1073 figiJSON = self.iList["Futures"][item] 1074 1075 if debug: 1076 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1077 1078 break 1079 1080 if figiJSON: 1081 self.figi = figiJSON["figi"] 1082 self.ticker = figiJSON["ticker"] 1083 1084 if requestPrice: 1085 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1086 1087 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1088 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1089 1090 else: 1091 figiJSON["currentPrice"]["changes"] = 0 1092 1093 if show: 1094 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1095 1096 else: 1097 if show: 1098 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1099 1100 return figiJSON 1101 1102 def GetCurrentPrices(self, show: bool = True) -> dict: 1103 """ 1104 Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record: 1105 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1106 1107 See also: 1108 1109 :param show: if `True` then print DOM to log and console. 1110 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1111 """ 1112 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1113 1114 if self.depth < 1: 1115 uLogger.error("Depth of Market (DOM) must be >=1!") 1116 raise Exception("Incorrect value") 1117 1118 if not (self.ticker or self.figi): 1119 uLogger.error("self.ticker or self.figi variables must be defined!") 1120 raise Exception("Ticker or FIGI required") 1121 1122 if self.ticker and not self.figi: 1123 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1124 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1125 1126 if not self.ticker and self.figi: 1127 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1128 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1129 1130 if not self.figi: 1131 uLogger.error("FIGI is not defined!") 1132 raise Exception("Ticker or FIGI required") 1133 1134 else: 1135 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1136 1137 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1138 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1139 self.body = str({"figi": self.figi, "depth": self.depth}) 1140 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") 1141 1142 if pricesResponse: 1143 # list of dicts with sellers orders: 1144 prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1145 1146 # list of dicts with buyers orders: 1147 prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1148 1149 # max price of instrument at this time: 1150 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1151 1152 # min price of instrument at this time: 1153 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1154 1155 # last price of deal with instrument: 1156 prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0 1157 1158 # last close price of instrument: 1159 prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0 1160 1161 else: 1162 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1163 uLogger.debug("Server response: {}".format(pricesResponse)) 1164 1165 if show: 1166 if prices["buy"] or prices["sell"]: 1167 info = [ 1168 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1169 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1170 self.ticker, 1171 self.figi, 1172 self.depth, 1173 ), 1174 uLog.sepShort, "\n", 1175 " Orders of Buyers | Orders of Sellers\n", 1176 uLog.sepShort, "\n", 1177 " Sell prices (vol.) | Buy prices (vol.)\n", 1178 uLog.sepShort, "\n", 1179 ] 1180 1181 if not prices["buy"]: 1182 info.append(" | No orders!\n") 1183 sumBuy = 0 1184 1185 else: 1186 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1187 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1188 for item in maxMinSorted: 1189 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1190 1191 if not prices["sell"]: 1192 info.append("No orders! |\n") 1193 sumSell = 0 1194 1195 else: 1196 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1197 for item in prices["sell"]: 1198 info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1199 1200 info.extend([ 1201 uLog.sepShort, "\n", 1202 "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1203 uLog.sepShort, "\n", 1204 ]) 1205 1206 infoText = "".join(info) 1207 1208 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1209 1210 else: 1211 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1212 1213 return prices 1214 1215 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1216 """ 1217 This method get and show information about all available broker instruments for current user account. 1218 If `instrumentsFile` string is not empty then also save information to this file. 1219 1220 :param show: if `True` then print results to console, if `False` - print only to file. 1221 :return: multi-lines string with all available broker instruments 1222 """ 1223 if not self.iList: 1224 self.iList = self.Listing() 1225 1226 info = [ 1227 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1228 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1229 ] 1230 1231 # add instruments count by type: 1232 for iType in self.iList.keys(): 1233 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1234 1235 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1236 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1237 1238 # generating info tables with all instruments by type: 1239 for iType in self.iList.keys(): 1240 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1241 1242 for instrument in self.iList[iType].keys(): 1243 iName = self.iList[iType][instrument]["name"] # instrument's name 1244 if len(iName) > 57: 1245 iName = "{}...".format(iName[:54]) # right trim for a long string 1246 1247 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1248 self.iList[iType][instrument]["ticker"], 1249 iName, 1250 self.iList[iType][instrument]["figi"], 1251 self.iList[iType][instrument]["currency"], 1252 self.iList[iType][instrument]["lot"], 1253 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1254 )) 1255 1256 infoText = "".join(info) 1257 1258 if show: 1259 uLogger.info(infoText) 1260 1261 if self.instrumentsFile: 1262 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1263 fH.write(infoText) 1264 1265 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1266 1267 return infoText 1268 1269 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1270 """ 1271 This method search and show information about instruments by part of its ticker, FIGI or name. 1272 If `searchResultsFile` string is not empty then also save information to this file. 1273 1274 :param pattern: string with part of ticker, FIGI or instrument's name. 1275 :param show: if `True` then print results to console, if `False` - return list of result only. 1276 :return: list of dictionaries with all found instruments. 1277 """ 1278 if not self.iList: 1279 self.iList = self.Listing() 1280 1281 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1282 compiledPattern = re.compile(pattern, re.IGNORECASE) 1283 1284 for iType in self.iList: 1285 for instrument in self.iList[iType].values(): 1286 searchResult = compiledPattern.search(" ".join( 1287 [instrument["ticker"], instrument["figi"], instrument["name"]] 1288 )) 1289 1290 if searchResult: 1291 searchResults[iType][instrument["ticker"]] = instrument 1292 1293 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1294 info = [ 1295 "# Search results\n\n", 1296 "* **Search pattern:** [{}]\n".format(pattern), 1297 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1298 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1299 ] 1300 infoShort = info[:] 1301 1302 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1303 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1304 skippedLine = "| ... | ... | ... | ... |\n" 1305 1306 if resultsLen == 0: 1307 info.append("\nNo results\n") 1308 infoShort.append("\nNo results\n") 1309 uLogger.warning("No results. Try changing your search pattern.") 1310 1311 else: 1312 for iType in searchResults: 1313 iTypeValuesCount = len(searchResults[iType].values()) 1314 if iTypeValuesCount > 0: 1315 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1316 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1317 1318 for instrument in searchResults[iType].values(): 1319 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1320 instrument["type"], 1321 instrument["ticker"], 1322 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1323 instrument["figi"], 1324 )) 1325 1326 if iTypeValuesCount <= 5: 1327 infoShort.extend(info[-iTypeValuesCount:]) 1328 1329 else: 1330 infoShort.extend(info[-5:]) 1331 infoShort.append(skippedLine) 1332 1333 infoText = "".join(info) 1334 infoTextShort = "".join(infoShort) 1335 1336 if show: 1337 uLogger.info(infoTextShort) 1338 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1339 1340 if self.searchResultsFile: 1341 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1342 fH.write(infoText) 1343 1344 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1345 1346 return searchResults 1347 1348 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1349 """ 1350 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1351 1352 :param instruments: list of strings with tickers or FIGIs. 1353 :return: list with unique instrument FIGIs only. 1354 """ 1355 requestedInstruments = [] 1356 for iName in instruments: 1357 if iName not in self.aliases.keys(): 1358 if iName not in requestedInstruments: 1359 requestedInstruments.append(iName) 1360 1361 else: 1362 if iName not in requestedInstruments: 1363 if self.aliases[iName] not in requestedInstruments: 1364 requestedInstruments.append(self.aliases[iName]) 1365 1366 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1367 1368 onlyUniqueFIGIs = [] 1369 for iName in requestedInstruments: 1370 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1371 continue 1372 1373 self.ticker = iName 1374 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1375 1376 if not iData: 1377 self.ticker = "" 1378 self.figi = iName 1379 1380 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1381 1382 if not iData: 1383 self.figi = "" 1384 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1385 1386 if iData and iData["figi"] not in onlyUniqueFIGIs: 1387 onlyUniqueFIGIs.append(iData["figi"]) 1388 1389 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1390 1391 return onlyUniqueFIGIs 1392 1393 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1394 """ 1395 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1396 See limits: https://tinkoff.github.io/investAPI/limits/ 1397 If `pricesFile` string is not empty then also save information to this file. 1398 1399 :param instruments: list of strings with tickers or FIGIs. 1400 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1401 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1402 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1403 """ 1404 if instruments is None or not instruments: 1405 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1406 raise Exception("Ticker or FIGI required") 1407 1408 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1409 1410 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1411 1412 iList = [] # trying to get info and current prices about all unique instruments: 1413 for self.figi in onlyUniqueFIGIs: 1414 iData = self.SearchByFIGI(requestPrice=True) 1415 iList.append(iData) 1416 1417 self.ShowListOfPrices(iList, show) 1418 1419 return iList 1420 1421 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1422 """ 1423 Show table contains current prices of given instruments. 1424 1425 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1426 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1427 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1428 :return: multilines text in Markdown format as a table contains current prices. 1429 """ 1430 infoText = "" 1431 1432 if show or self.pricesFile: 1433 info = [ 1434 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1435 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1436 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1437 ] 1438 1439 for item in iList: 1440 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1441 item["ticker"], 1442 item["figi"], 1443 item["type"], 1444 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1445 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1446 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1447 "{} / {}".format( 1448 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1449 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1450 ), 1451 "{} / {}".format( 1452 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1453 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1454 ), 1455 item["currency"], 1456 )) 1457 1458 infoText = "".join(info) 1459 1460 if show: 1461 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1462 1463 if self.pricesFile: 1464 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1465 fH.write(infoText) 1466 1467 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1468 1469 return infoText 1470 1471 def RequestTradingStatus(self) -> dict: 1472 """ 1473 Requesting trading status for the instrument defined by `figi` variable. 1474 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1475 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1476 1477 :return: dictionary with trading status attributes. Response example: 1478 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1479 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1480 """ 1481 if self.figi is None or not self.figi: 1482 uLogger.error("Variable `figi` must be defined for using this method!") 1483 raise Exception("FIGI required") 1484 1485 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1486 1487 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1488 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1489 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1490 1491 uLogger.debug("Records about current trading status successfully received") 1492 1493 return tradingStatus 1494 1495 def RequestPortfolio(self) -> dict: 1496 """ 1497 Requesting actual user's portfolio for current `accountId`. 1498 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1499 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1500 1501 :return: dictionary with user's portfolio. 1502 """ 1503 if self.accountId is None or not self.accountId: 1504 uLogger.error("Variable `accountId` must be defined for using this method!") 1505 raise Exception("Account ID required") 1506 1507 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1508 1509 self.body = str({"accountId": self.accountId}) 1510 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1511 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1512 1513 uLogger.debug("Records about user's portfolio successfully received") 1514 1515 return rawPortfolio 1516 1517 def RequestPositions(self) -> dict: 1518 """ 1519 Requesting open positions by currencies and instruments for current `accountId`. 1520 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1521 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1522 1523 :return: dictionary with open positions by instruments. 1524 """ 1525 if self.accountId is None or not self.accountId: 1526 uLogger.error("Variable `accountId` must be defined for using this method!") 1527 raise Exception("Account ID required") 1528 1529 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1530 1531 self.body = str({"accountId": self.accountId}) 1532 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1533 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1534 1535 uLogger.debug("Records about current open positions successfully received") 1536 1537 return rawPositions 1538 1539 def RequestPendingOrders(self) -> list: 1540 """ 1541 Requesting current actual pending orders for current `accountId`. 1542 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1543 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1544 1545 :return: list of dictionaries with pending orders. 1546 """ 1547 if self.accountId is None or not self.accountId: 1548 uLogger.error("Variable `accountId` must be defined for using this method!") 1549 raise Exception("Account ID required") 1550 1551 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1552 1553 self.body = str({"accountId": self.accountId}) 1554 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1555 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1556 1557 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1558 1559 return rawOrders 1560 1561 def RequestStopOrders(self) -> list: 1562 """ 1563 Requesting current actual stop orders for current `accountId`. 1564 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1565 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1566 1567 :return: list of dictionaries with stop orders. 1568 """ 1569 if self.accountId is None or not self.accountId: 1570 uLogger.error("Variable `accountId` must be defined for using this method!") 1571 raise Exception("Account ID required") 1572 1573 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1574 1575 self.body = str({"accountId": self.accountId}) 1576 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1577 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1578 1579 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1580 1581 return rawStopOrders 1582 1583 def Overview(self, show: bool = False, details: str = "full") -> dict: 1584 """ 1585 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1586 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1587 are defined then also save information to file. 1588 1589 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1590 many requests about the state of the portfolio, and then, based on the received data, a large number 1591 of calculation and statistics are collected. 1592 1593 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1594 :param details: how detailed should the information be? You should specify one of strings: 1595 `full` - shows full available information about portfolio status (by default), 1596 `positions` - shows only open positions, 1597 `digest` - show a short digest of the portfolio status, 1598 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1599 `orders` - shows only sections of open limits and stop orders. 1600 :return: dictionary with client's raw portfolio and some statistics. 1601 """ 1602 if self.accountId is None or not self.accountId: 1603 uLogger.error("Variable `accountId` must be defined for using this method!") 1604 raise Exception("Account ID required") 1605 1606 view = { 1607 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1608 "headers": {}, # list of dictionaries, response headers without "positions" section 1609 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1610 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1611 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1612 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1613 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1614 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1615 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1616 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1617 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1618 }, 1619 "stat": { # --- some statistics calculated using "raw" sections: 1620 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1621 "availableRUB": 0., # available rubles (without other currencies) 1622 "blockedRUB": 0., # blocked sum in Russian Rouble 1623 "totalChangesRUB": 0., # changes for all open trades in RUB 1624 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1625 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1626 "sharesCostRUB": 0., # costs of all shares in RUB 1627 "bondsCostRUB": 0., # costs of all bonds in RUB 1628 "etfsCostRUB": 0., # costs of all etfs in RUB 1629 "futuresCostRUB": 0., # costs of all futures in RUB 1630 "Currencies": [], # list of dictionaries of all currencies statistics 1631 "Shares": [], # list of dictionaries of all shares statistics 1632 "Bonds": [], # list of dictionaries of all bonds statistics 1633 "Etfs": [], # list of dictionaries of all etfs statistics 1634 "Futures": [], # list of dictionaries of all futures statistics 1635 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1636 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1637 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1638 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1639 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1640 }, 1641 "analytics": { # --- some analytics of portfolio: 1642 "distrByAssets": {}, # portfolio distribution by assets 1643 "distrByCompanies": {}, # portfolio distribution by companies 1644 "distrBySectors": {}, # portfolio distribution by sectors 1645 "distrByCurrencies": {}, # portfolio distribution by currencies 1646 "distrByCountries": {}, # portfolio distribution by countries 1647 } 1648 } 1649 1650 details = details.lower() 1651 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1652 if details not in availableDetails: 1653 details = "full" 1654 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1655 1656 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1657 1658 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1659 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1660 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1661 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1662 1663 # save response headers without "positions" section: 1664 for key in portfolioResponse.keys(): 1665 if key != "positions": 1666 view["raw"]["headers"][key] = portfolioResponse[key] 1667 1668 else: 1669 continue 1670 1671 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1672 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1673 for item in portfolioResponse["positions"]: 1674 if item["instrumentType"] == "currency": 1675 self.figi = item["figi"] 1676 curr = self.SearchByFIGI(requestPrice=False) 1677 1678 # current price of currency in RUB: 1679 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1680 "name": curr["name"], 1681 "currentPrice": NanoToFloat( 1682 item["currentPrice"]["units"], 1683 item["currentPrice"]["nano"] 1684 ), 1685 } 1686 1687 view["raw"]["Currencies"].append(item) 1688 1689 elif item["instrumentType"] == "share": 1690 view["raw"]["Shares"].append(item) 1691 1692 elif item["instrumentType"] == "bond": 1693 view["raw"]["Bonds"].append(item) 1694 1695 elif item["instrumentType"] == "etf": 1696 view["raw"]["Etfs"].append(item) 1697 1698 elif item["instrumentType"] == "futures": 1699 view["raw"]["Futures"].append(item) 1700 1701 else: 1702 continue 1703 1704 # how many volume of currencies (by ISO currency name) are blocked: 1705 for item in view["raw"]["positions"]["blocked"]: 1706 blocked = NanoToFloat(item["units"], item["nano"]) 1707 if blocked > 0: 1708 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1709 1710 # how many volume of instruments (by FIGI) are blocked: 1711 for item in view["raw"]["positions"]["securities"]: 1712 blocked = int(item["blocked"]) 1713 if blocked > 0: 1714 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1715 1716 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1717 1718 if "rub" in allBlocked.keys(): 1719 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1720 1721 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1722 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1723 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1724 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1725 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1726 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1727 view["stat"]["portfolioCostRUB"] = sum([ 1728 view["stat"]["allCurrenciesCostRUB"], 1729 view["stat"]["sharesCostRUB"], 1730 view["stat"]["bondsCostRUB"], 1731 view["stat"]["etfsCostRUB"], 1732 view["stat"]["futuresCostRUB"], 1733 ]) 1734 1735 # --- calculating some portfolio statistics: 1736 byComp = {} # distribution by companies 1737 bySect = {} # distribution by sectors 1738 byCurr = {} # distribution by currencies (include RUB) 1739 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1740 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1741 1742 for item in portfolioResponse["positions"]: 1743 self.figi = item["figi"] 1744 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1745 1746 if instrument: 1747 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1748 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1749 1750 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1751 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1752 1753 else: 1754 blocked = 0 1755 1756 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1757 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1758 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1759 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1760 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1761 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1762 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1763 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1764 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1765 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1766 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1767 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1768 1769 statData = { 1770 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1771 "ticker": instrument["ticker"], # ticker by FIGI 1772 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1773 "volume": volume, # available volume of instrument 1774 "lots": lots, # volume in lots of instrument 1775 "direction": direction, # direction of an instrument's position: short or long 1776 "blocked": blocked, # blocked volume of currency or instrument 1777 "currentPrice": curPrice, # current instrument's price in basic asset 1778 "average": average, # current average position price 1779 "cost": cost, # current cost of all volume of instrument in basic asset 1780 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1781 "costRUB": costRUB, # cost of instrument in ruble 1782 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1783 "profit": profit, # expected profit at current moment 1784 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1785 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1786 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1787 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1788 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1789 "step": instrument["step"], # minimum price increment 1790 } 1791 1792 # adding distribution by unique countries: 1793 if statData["country"] not in byCountry.keys(): 1794 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1795 1796 else: 1797 byCountry[statData["country"]]["cost"] += costRUB 1798 byCountry[statData["country"]]["percent"] += percentCostRUB 1799 1800 if item["instrumentType"] != "currency": 1801 # adding distribution by unique companies: 1802 if statData["name"]: 1803 if statData["name"] not in byComp.keys(): 1804 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1805 1806 else: 1807 byComp[statData["name"]]["cost"] += costRUB 1808 byComp[statData["name"]]["percent"] += percentCostRUB 1809 1810 # adding distribution by unique sectors: 1811 if statData["sector"] not in bySect.keys(): 1812 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1813 1814 else: 1815 bySect[statData["sector"]]["cost"] += costRUB 1816 bySect[statData["sector"]]["percent"] += percentCostRUB 1817 1818 # adding distribution by unique currencies: 1819 if currency not in byCurr.keys(): 1820 byCurr[currency] = { 1821 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1822 "cost": costRUB, 1823 "percent": percentCostRUB 1824 } 1825 1826 else: 1827 byCurr[currency]["cost"] += costRUB 1828 byCurr[currency]["percent"] += percentCostRUB 1829 1830 # saving statistics for every instrument: 1831 if item["instrumentType"] == "currency": 1832 view["stat"]["Currencies"].append(statData) 1833 1834 # update dict with free funds for trading (total - blocked) by currencies 1835 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1836 view["stat"]["funds"][currency] = { 1837 "total": volume, 1838 "totalCostRUB": costRUB, # total volume cost in rubles 1839 "free": volume - blocked, 1840 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1841 } 1842 1843 elif item["instrumentType"] == "share": 1844 view["stat"]["Shares"].append(statData) 1845 1846 elif item["instrumentType"] == "bond": 1847 view["stat"]["Bonds"].append(statData) 1848 1849 elif item["instrumentType"] == "etf": 1850 view["stat"]["Etfs"].append(statData) 1851 1852 elif item["instrumentType"] == "Futures": 1853 view["stat"]["Futures"].append(statData) 1854 1855 else: 1856 continue 1857 1858 # total changes in Russian Ruble: 1859 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1860 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1861 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1862 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1863 view["stat"]["funds"]["rub"] = { 1864 "total": view["stat"]["availableRUB"], 1865 "totalCostRUB": view["stat"]["availableRUB"], 1866 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1867 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1868 } 1869 1870 # --- pending orders sector data: 1871 uniquePendingOrders = [] 1872 uniquePendingOrdersFIGIs = [] 1873 for item in view["raw"]["orders"]: 1874 if item["figi"] not in uniquePendingOrdersFIGIs: 1875 uniquePendingOrdersFIGIs.append(item["figi"]) 1876 uniquePendingOrders.append(item) 1877 1878 for item in uniquePendingOrders: 1879 self.figi = item["figi"] 1880 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1881 1882 if instrument: 1883 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1884 orderType = TKS_ORDER_TYPES[item["orderType"]] 1885 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1886 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1887 1888 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1889 if item["direction"] == "ORDER_DIRECTION_BUY": 1890 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1891 1892 else: 1893 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1894 1895 # requested price for order execution: 1896 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1897 1898 # necessary changes in percent to reach target from current price: 1899 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1900 1901 view["stat"]["orders"].append({ 1902 "orderID": item["orderId"], # orderId number parameter of current order 1903 "figi": item["figi"], # FIGI identification 1904 "ticker": instrument["ticker"], # ticker name by FIGI 1905 "lotsRequested": item["lotsRequested"], # requested lots value 1906 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1907 "currentPrice": lastPrice, # current instrument's price for defined action 1908 "targetPrice": target, # requested price for order execution in base currency 1909 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1910 "percentChanges": changes, # changes in percent to target from current price 1911 "currency": item["currency"], # instrument's currency name 1912 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1913 "type": orderType, # type of order from TKS_ORDER_TYPES 1914 "status": orderState, # order status from TKS_ORDER_STATES 1915 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1916 }) 1917 1918 # --- stop orders sector data: 1919 uniqueStopOrders = [] 1920 uniqueStopOrdersFIGIs = [] 1921 for item in view["raw"]["stopOrders"]: 1922 if item["figi"] not in uniqueStopOrdersFIGIs: 1923 uniqueStopOrdersFIGIs.append(item["figi"]) 1924 uniqueStopOrders.append(item) 1925 1926 for item in uniqueStopOrders: 1927 self.figi = item["figi"] 1928 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1929 1930 if instrument: 1931 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1932 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1933 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1934 1935 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1936 if "expirationTime" in item.keys(): 1937 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1938 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1939 1940 else: 1941 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1942 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1943 1944 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1945 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1946 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1947 1948 else: 1949 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1950 1951 # requested price when stop-order executed: 1952 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1953 1954 # price for limit-order, set up when stop-order executed: 1955 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1956 1957 # necessary changes in percent to reach target from current price: 1958 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1959 1960 view["stat"]["stopOrders"].append({ 1961 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1962 "figi": item["figi"], # FIGI identification 1963 "ticker": instrument["ticker"], # ticker name by FIGI 1964 "lotsRequested": item["lotsRequested"], # requested lots value 1965 "currentPrice": lastPrice, # current instrument's price for defined action 1966 "targetPrice": target, # requested price for stop-order execution in base currency 1967 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1968 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1969 "percentChanges": changes, # changes in percent to target from current price 1970 "currency": item["currency"], # instrument's currency name 1971 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1972 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1973 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1974 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1975 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1976 }) 1977 1978 # --- calculating data for analytics section: 1979 # portfolio distribution by assets: 1980 view["analytics"]["distrByAssets"] = { 1981 "Ruble": { 1982 "uniques": 1, 1983 "cost": view["stat"]["availableRUB"], 1984 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1985 }, 1986 "Currencies": { 1987 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1988 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1989 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1990 }, 1991 "Shares": { 1992 "uniques": len(view["stat"]["Shares"]), 1993 "cost": view["stat"]["sharesCostRUB"], 1994 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1995 }, 1996 "Bonds": { 1997 "uniques": len(view["stat"]["Bonds"]), 1998 "cost": view["stat"]["bondsCostRUB"], 1999 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2000 }, 2001 "Etfs": { 2002 "uniques": len(view["stat"]["Etfs"]), 2003 "cost": view["stat"]["etfsCostRUB"], 2004 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2005 }, 2006 "Futures": { 2007 "uniques": len(view["stat"]["Futures"]), 2008 "cost": view["stat"]["futuresCostRUB"], 2009 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2010 }, 2011 } 2012 2013 # portfolio distribution by companies: 2014 view["analytics"]["distrByCompanies"]["All money cash"] = { 2015 "ticker": "", 2016 "cost": view["stat"]["allCurrenciesCostRUB"], 2017 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2018 } 2019 view["analytics"]["distrByCompanies"].update(byComp) 2020 2021 # portfolio distribution by sectors: 2022 view["analytics"]["distrBySectors"]["All money cash"] = { 2023 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2024 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2025 } 2026 view["analytics"]["distrBySectors"].update(bySect) 2027 2028 # portfolio distribution by currencies: 2029 view["analytics"]["distrByCurrencies"].update(byCurr) 2030 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2031 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2032 2033 # portfolio distribution by countries: 2034 view["analytics"]["distrByCountries"].update(byCountry) 2035 2036 # --- Prepare text statistics overview in human-readable: 2037 if show: 2038 # Whatever the value `details`, header not changes: 2039 info = [ 2040 "# Client's portfolio\n\n", 2041 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2042 "* **Account ID:** [{}]\n".format(self.accountId), 2043 ] 2044 2045 if details in ["full", "positions", "digest"]: 2046 info.extend([ 2047 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2048 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2049 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2050 view["stat"]["totalChangesRUB"], 2051 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2052 view["stat"]["totalChangesPercentRUB"], 2053 ), 2054 ]) 2055 2056 if details in ["full", "positions"]: 2057 info.extend([ 2058 "## Open positions\n\n", 2059 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2060 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2061 "| Ruble | {:>31} | | | | | |\n".format( 2062 "{:.2f} ({:.2f}) rub".format( 2063 view["stat"]["availableRUB"], 2064 view["stat"]["blockedRUB"], 2065 ) 2066 ) 2067 ]) 2068 2069 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2070 return [ 2071 "| | | | | | | |\n", 2072 "| {:<27} | | | | | {:>19} | |\n".format( 2073 noTradeStr if noTradeStr else typeStr, 2074 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2075 ), 2076 ] 2077 2078 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2079 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2080 "{} [{}]".format(data["ticker"], data["figi"]), 2081 "{:.2f} ({:.2f}) {}".format( 2082 data["volume"], 2083 data["blocked"], 2084 data["currency"], 2085 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2086 data["volume"], 2087 data["blocked"], 2088 ), 2089 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2090 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2091 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2092 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2093 "{}{:.2f} {} ({}{:.2f}%)".format( 2094 "+" if data["profit"] > 0 else "", 2095 data["profit"], data["baseCurrencyName"], 2096 "+" if data["percentProfit"] > 0 else "", 2097 data["percentProfit"], 2098 ), 2099 ) 2100 2101 # --- Show currencies section: 2102 if view["stat"]["Currencies"]: 2103 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2104 for item in view["stat"]["Currencies"]: 2105 info.append(_InfoStr(item, showCurrencyName=True)) 2106 2107 else: 2108 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2109 2110 # --- Show shares section: 2111 if view["stat"]["Shares"]: 2112 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2113 2114 for item in view["stat"]["Shares"]: 2115 info.append(_InfoStr(item)) 2116 2117 else: 2118 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2119 2120 # --- Show bonds section: 2121 if view["stat"]["Bonds"]: 2122 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2123 2124 for item in view["stat"]["Bonds"]: 2125 info.append(_InfoStr(item)) 2126 2127 else: 2128 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2129 2130 # --- Show etfs section: 2131 if view["stat"]["Etfs"]: 2132 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2133 2134 for item in view["stat"]["Etfs"]: 2135 info.append(_InfoStr(item)) 2136 2137 else: 2138 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2139 2140 # --- Show futures section: 2141 if view["stat"]["Futures"]: 2142 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2143 2144 for item in view["stat"]["Futures"]: 2145 info.append(_InfoStr(item)) 2146 2147 else: 2148 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2149 2150 if details in ["full", "orders"]: 2151 # --- Show pending orders section: 2152 if view["stat"]["orders"]: 2153 info.extend([ 2154 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2155 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2156 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2157 ]) 2158 2159 for item in view["stat"]["orders"]: 2160 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2161 "{} [{}]".format(item["ticker"], item["figi"]), 2162 item["orderID"], 2163 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2164 "{} {} ({}{:.2f}%)".format( 2165 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2166 item["baseCurrencyName"], 2167 "+" if item["percentChanges"] > 0 else "", 2168 float(item["percentChanges"]), 2169 ), 2170 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2171 item["action"], 2172 item["type"], 2173 item["date"], 2174 )) 2175 2176 else: 2177 info.append("\n## Total pending limit-orders: 0\n") 2178 2179 # --- Show stop orders section: 2180 if view["stat"]["stopOrders"]: 2181 info.extend([ 2182 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2183 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2184 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2185 ]) 2186 2187 for item in view["stat"]["stopOrders"]: 2188 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2189 "{} [{}]".format(item["ticker"], item["figi"]), 2190 item["orderID"], 2191 item["lotsRequested"], 2192 "{} {} ({}{:.2f}%)".format( 2193 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2194 item["baseCurrencyName"], 2195 "+" if item["percentChanges"] > 0 else "", 2196 float(item["percentChanges"]), 2197 ), 2198 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2199 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2200 item["action"], 2201 item["type"], 2202 item["expType"], 2203 item["createDate"], 2204 item["expDate"], 2205 )) 2206 2207 else: 2208 info.append("\n## Total stop-orders: 0\n") 2209 2210 if details in ["full", "analytics"]: 2211 # -- Show analytics section: 2212 if view["stat"]["portfolioCostRUB"] > 0: 2213 info.extend([ 2214 "\n# Analytics\n" 2215 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2216 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2217 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2218 view["stat"]["totalChangesRUB"], 2219 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2220 view["stat"]["totalChangesPercentRUB"], 2221 ), 2222 "\n## Portfolio distribution by assets\n" 2223 "\n| Type | Uniques | Percent | Current cost |\n", 2224 "|------------|---------|---------|--------------------|\n", 2225 ]) 2226 2227 for key in view["analytics"]["distrByAssets"].keys(): 2228 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2229 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2230 key, 2231 view["analytics"]["distrByAssets"][key]["uniques"], 2232 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2233 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2234 )) 2235 2236 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2237 info.extend([ 2238 "\n## Portfolio distribution by companies\n" 2239 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2240 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2241 ]) 2242 2243 for company in view["analytics"]["distrByCompanies"].keys(): 2244 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2245 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2246 info.append("| {} | {:<7} | {:<18} |\n".format( 2247 "{}{}{}".format( 2248 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2249 company, 2250 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2251 ), 2252 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2253 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2254 )) 2255 2256 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2257 info.extend([ 2258 "\n## Portfolio distribution by sectors\n" 2259 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2260 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2261 ]) 2262 2263 for sector in view["analytics"]["distrBySectors"].keys(): 2264 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2265 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2266 sector, 2267 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2268 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2269 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2270 )) 2271 2272 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2273 info.extend([ 2274 "\n## Portfolio distribution by currencies\n" 2275 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2276 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2277 ]) 2278 2279 for curr in view["analytics"]["distrByCurrencies"].keys(): 2280 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2281 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2282 info.append("| {} | {:<7} | {:<18} |\n".format( 2283 "[{}] {}{}".format( 2284 curr, 2285 view["analytics"]["distrByCurrencies"][curr]["name"], 2286 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2287 ), 2288 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2289 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2290 )) 2291 2292 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2293 info.extend([ 2294 "\n## Portfolio distribution by countries\n" 2295 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2296 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2297 ]) 2298 2299 for country in view["analytics"]["distrByCountries"].keys(): 2300 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2301 nameLen = len(country) 2302 info.append("| {} | {:<7} | {:<18} |\n".format( 2303 "{}{}".format( 2304 country, 2305 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2306 ), 2307 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2308 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2309 )) 2310 2311 infoText = "".join(info) 2312 2313 uLogger.info(infoText) 2314 2315 if details == "full" and self.overviewFile: 2316 filename = self.overviewFile 2317 2318 elif details == "digest" and self.overviewDigestFile: 2319 filename = self.overviewDigestFile 2320 2321 elif details == "positions" and self.overviewPositionsFile: 2322 filename = self.overviewPositionsFile 2323 2324 elif details == "orders" and self.overviewOrdersFile: 2325 filename = self.overviewOrdersFile 2326 2327 elif details == "analytics" and self.overviewAnalyticsFile: 2328 filename = self.overviewAnalyticsFile 2329 2330 else: 2331 filename = "" 2332 2333 if filename: 2334 with open(filename, "w", encoding="UTF-8") as fH: 2335 fH.write(infoText) 2336 2337 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2338 2339 return view 2340 2341 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2342 """ 2343 Returns history operations between two given dates for current `accountId`. 2344 If `reportFile` string is not empty then also save human-readable report. 2345 Shows some statistical data of closed positions. 2346 2347 :param start: see docstring in `GetDatesAsString()` method 2348 :param end: see docstring in `GetDatesAsString()` method 2349 :param show: if `True` then also prints all records to the console. 2350 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2351 :return: original list of dictionaries with history of deals records from API ("operations" key): 2352 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2353 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2354 """ 2355 if self.accountId is None or not self.accountId: 2356 uLogger.error("Variable `accountId` must be defined for using this method!") 2357 raise Exception("Account ID required") 2358 2359 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2360 2361 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2362 2363 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2364 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2365 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2366 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2367 customStat = {} # custom statistics in additional to responseJSON 2368 2369 # --- output report in human-readable format: 2370 if show or self.reportFile: 2371 splitLine1 = "| | | | | |\n" # Summary section 2372 splitLine2 = "| | | | | | | | |\n" # Operations section 2373 nextDay = "" 2374 2375 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2376 2377 if len(ops) > 0: 2378 customStat = { 2379 "opsCount": 0, # total operations count 2380 "buyCount": 0, # buy operations 2381 "sellCount": 0, # sell operations 2382 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2383 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2384 "payIn": {"rub": 0.}, # Deposit brokerage account 2385 "payOut": {"rub": 0.}, # Withdrawals 2386 "divs": {"rub": 0.}, # Dividends income 2387 "coupons": {"rub": 0.}, # Coupon's income 2388 "brokerCom": {"rub": 0.}, # Service commissions 2389 "serviceCom": {"rub": 0.}, # Service commissions 2390 "marginCom": {"rub": 0.}, # Margin commissions 2391 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2392 } 2393 2394 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2395 for item in ops: 2396 if item["state"] == "OPERATION_STATE_EXECUTED": 2397 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2398 2399 # count buy operations: 2400 if "_BUY" in item["operationType"]: 2401 customStat["buyCount"] += 1 2402 2403 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2404 customStat["buyTotal"][item["payment"]["currency"]] += payment 2405 2406 else: 2407 customStat["buyTotal"][item["payment"]["currency"]] = payment 2408 2409 # count sell operations: 2410 elif "_SELL" in item["operationType"]: 2411 customStat["sellCount"] += 1 2412 2413 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2414 customStat["sellTotal"][item["payment"]["currency"]] += payment 2415 2416 else: 2417 customStat["sellTotal"][item["payment"]["currency"]] = payment 2418 2419 # count incoming operations: 2420 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2421 if item["payment"]["currency"] in customStat["payIn"].keys(): 2422 customStat["payIn"][item["payment"]["currency"]] += payment 2423 2424 else: 2425 customStat["payIn"][item["payment"]["currency"]] = payment 2426 2427 # count withdrawals operations: 2428 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2429 if item["payment"]["currency"] in customStat["payOut"].keys(): 2430 customStat["payOut"][item["payment"]["currency"]] += payment 2431 2432 else: 2433 customStat["payOut"][item["payment"]["currency"]] = payment 2434 2435 # count dividends income: 2436 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2437 if item["payment"]["currency"] in customStat["divs"].keys(): 2438 customStat["divs"][item["payment"]["currency"]] += payment 2439 2440 else: 2441 customStat["divs"][item["payment"]["currency"]] = payment 2442 2443 # count coupon's income: 2444 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2445 if item["payment"]["currency"] in customStat["coupons"].keys(): 2446 customStat["coupons"][item["payment"]["currency"]] += payment 2447 2448 else: 2449 customStat["coupons"][item["payment"]["currency"]] = payment 2450 2451 # count broker commissions: 2452 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2453 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2454 customStat["brokerCom"][item["payment"]["currency"]] += payment 2455 2456 else: 2457 customStat["brokerCom"][item["payment"]["currency"]] = payment 2458 2459 # count service commissions: 2460 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2461 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2462 customStat["serviceCom"][item["payment"]["currency"]] += payment 2463 2464 else: 2465 customStat["serviceCom"][item["payment"]["currency"]] = payment 2466 2467 # count margin commissions: 2468 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2469 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2470 customStat["marginCom"][item["payment"]["currency"]] += payment 2471 2472 else: 2473 customStat["marginCom"][item["payment"]["currency"]] = payment 2474 2475 # count withholding taxes: 2476 elif "_TAX" in item["operationType"]: 2477 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2478 customStat["allTaxes"][item["payment"]["currency"]] += payment 2479 2480 else: 2481 customStat["allTaxes"][item["payment"]["currency"]] = payment 2482 2483 else: 2484 continue 2485 2486 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2487 2488 # --- view "Actions" lines: 2489 info.extend([ 2490 "| 1 | 2 | 3 | 4 | 5 |\n", 2491 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2492 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2493 "| | Buy: {:<22} | {:<28} | | |\n".format( 2494 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2495 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2496 ), 2497 "| | Sell: {:<21} | {:<28} | | |\n".format( 2498 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2499 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2500 ), 2501 ]) 2502 2503 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2504 for key in opsKeys: 2505 if key == "rub": 2506 continue 2507 2508 info.extend([ 2509 "| | | {:<28} | | |\n".format( 2510 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2511 ), 2512 "| | | {:<28} | | |\n".format( 2513 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2514 ), 2515 ]) 2516 2517 info.append(splitLine1) 2518 2519 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2520 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2521 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2522 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2523 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2524 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2525 ) 2526 2527 # --- view "Payments" lines: 2528 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2529 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2530 2531 for key in paymentsKeys: 2532 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2533 2534 info.append(splitLine1) 2535 2536 # --- view "Commissions and taxes" lines: 2537 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2538 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2539 2540 for key in comKeys: 2541 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2542 2543 info.append(splitLine1) 2544 2545 info.extend([ 2546 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2547 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2548 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2549 ]) 2550 2551 else: 2552 info.append("Broker returned no operations during this period\n") 2553 2554 # --- view "Operations" section: 2555 for item in ops: 2556 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2557 continue 2558 2559 else: 2560 self.figi = item["figi"] if item["figi"] else "" 2561 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2562 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2563 2564 # group of deals during one day: 2565 if nextDay and item["date"].split("T")[0] != nextDay: 2566 info.append(splitLine2) 2567 nextDay = "" 2568 2569 else: 2570 nextDay = item["date"].split("T")[0] # saving current day for splitting 2571 2572 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2573 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2574 self.figi if self.figi else "—", 2575 instrument["ticker"] if instrument else "—", 2576 instrument["type"] if instrument else "—", 2577 item["quantity"] if int(item["quantity"]) > 0 else "—", 2578 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2579 TKS_OPERATION_STATES[item["state"]], 2580 TKS_OPERATION_TYPES[item["operationType"]], 2581 )) 2582 2583 infoText = "".join(info) 2584 2585 if show: 2586 uLogger.info(infoText) 2587 2588 if self.reportFile: 2589 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2590 fH.write(infoText) 2591 2592 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2593 2594 return ops, customStat 2595 2596 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2597 """ 2598 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2599 2600 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2601 Warning! Broker server used ISO UTC time by default. 2602 2603 If `historyFile` is not `None` then method save history to file, otherwise return only pandas dataframe. 2604 Also, `historyFile` used to update history with `onlyMissing` parameter. 2605 2606 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2607 2608 :param start: see docstring in `GetDatesAsString()` method. 2609 :param end: see docstring in `GetDatesAsString()` method. 2610 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2611 `"hour"`, `"day"`. Default: `"hour"`. 2612 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2613 False by default. Warning! History appends only from last candle to current time 2614 with always update last candle! 2615 :param csvSep: separator if csv-file is used, `,` by default. 2616 :param show: if `True` then also prints pandas dataframe to the console. 2617 :return: pandas dataframe with prices history. Headers of columns are defined by default: 2618 `["date", "time", "open", "high", "low", "close", "volume"]`. 2619 """ 2620 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2621 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2622 history = None # empty pandas object for history 2623 2624 if interval not in TKS_CANDLE_INTERVALS.keys(): 2625 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2626 raise Exception("Incorrect value") 2627 2628 if not (self.ticker or self.figi): 2629 uLogger.error("Ticker or FIGI must be defined!") 2630 raise Exception("Ticker or FIGI required") 2631 2632 if self.ticker and not self.figi: 2633 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2634 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2635 2636 if self.figi and not self.ticker: 2637 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2638 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2639 2640 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2641 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2642 if interval.lower() != "day": 2643 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2644 2645 delta = dtEnd - dtStart # current UTC time minus last time in file 2646 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2647 2648 # calculate history length in candles: 2649 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2650 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2651 length += 1 # to avoid fraction time 2652 2653 # calculate data blocks count: 2654 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2655 2656 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2657 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2658 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2659 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2660 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2661 2662 tempOld = None # pandas object for old history, if --only-missing key present 2663 lastTime = None # datetime object of last old candle in file 2664 2665 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2666 uLogger.debug("--only-missing key present, add only last missing candles...") 2667 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2668 2669 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2670 2671 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2672 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2673 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2674 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2675 2676 # get last datetime object from last string in file or minus 1 delta if file is empty: 2677 if len(tempOld) > 0: 2678 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2679 2680 else: 2681 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2682 2683 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2684 2685 responseJSONs = [] # raw history blocks of data 2686 2687 blockEnd = dtEnd 2688 for item in range(blocks): 2689 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2690 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2691 2692 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2693 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2694 )) 2695 2696 if blockStart == blockEnd: 2697 uLogger.debug("Skipped this zero-length block...") 2698 2699 else: 2700 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2701 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2702 self.body = str({ 2703 "figi": self.figi, 2704 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2705 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2706 "interval": TKS_CANDLE_INTERVALS[interval][0] 2707 }) 2708 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2709 2710 if "code" in responseJSON.keys(): 2711 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2712 2713 else: 2714 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2715 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2716 2717 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2718 2719 blockEnd = blockStart 2720 2721 printCount = len(responseJSONs) # candles to show in console 2722 if responseJSONs: 2723 tempHistory = pd.DataFrame( 2724 data={ 2725 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2726 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2727 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2728 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2729 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2730 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2731 "volume": [int(item["volume"]) for item in responseJSONs], 2732 }, 2733 index=range(len(responseJSONs)), 2734 columns=["date", "time", "open", "high", "low", "close", "volume"], 2735 ) 2736 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2737 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2738 2739 # append only newest candles to old history if --only-missing key present: 2740 if onlyMissing and tempOld is not None and lastTime is not None: 2741 index = 0 # find start index in tempHistory data: 2742 2743 for i, item in tempHistory.iterrows(): 2744 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2745 2746 if curTime == lastTime: 2747 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2748 index = i 2749 printCount = index + 1 2750 break 2751 2752 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2753 2754 else: 2755 history = tempHistory # if no `--only-missing` key then load full data from server 2756 2757 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2758 2759 if history is not None and not history.empty: 2760 if show: 2761 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2762 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2763 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2764 )) 2765 2766 else: 2767 uLogger.warning("Received an empty candles history!") 2768 2769 if self.historyFile is not None: 2770 if history is not None and not history.empty: 2771 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2772 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2773 2774 else: 2775 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2776 2777 else: 2778 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only pandas dataframe returns.") 2779 2780 return history 2781 2782 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2783 """ 2784 Load candles history from csv-file and return pandas dataframe object. 2785 2786 See also: `History()` and `ShowHistoryChart()` methods. 2787 2788 :param filePath: path to csv-file to open. 2789 """ 2790 loadedHistory = None # init candles data object 2791 2792 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2793 2794 if os.path.exists(filePath): 2795 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as pandas dataframe 2796 2797 tfStr = self.priceModel.FormattedDelta( 2798 self.priceModel.timeframe, 2799 "{days} days {hours}h {minutes}m {seconds}s", 2800 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2801 self.priceModel.timeframe, 2802 "{hours}h {minutes}m {seconds}s", 2803 ) 2804 2805 if loadedHistory is not None and not loadedHistory.empty: 2806 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2807 len(loadedHistory), 2808 tfStr, 2809 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2810 ) 2811 2812 else: 2813 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2814 2815 else: 2816 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2817 2818 return loadedHistory 2819 2820 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2821 """ 2822 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2823 2824 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2825 Default: `index.html` (both for interact and non-interact candlesticks chart). 2826 2827 See also: `History()` and `LoadHistory()` methods. 2828 2829 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2830 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2831 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2832 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2833 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2834 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2835 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2836 """ 2837 if isinstance(candles, str): 2838 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2839 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2840 2841 elif isinstance(candles, pd.DataFrame): 2842 self.priceModel.prices = candles # set candles chain from variable 2843 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2844 2845 if "datetime" not in candles.columns: 2846 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2847 2848 else: 2849 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2850 raise Exception("Incorrect value") 2851 2852 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2853 2854 if interact: 2855 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2856 2857 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2858 2859 else: 2860 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2861 2862 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2863 2864 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2865 2866 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2867 """ 2868 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2869 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2870 2871 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2872 2873 :param operation: string "Buy" or "Sell". 2874 :param lots: volume, integer count of lots >= 1. 2875 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2876 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2877 :param expDate: string "Undefined" by default or local date in future, 2878 it is a string with format `%Y-%m-%d %H:%M:%S`. 2879 :return: JSON with response from broker server. 2880 """ 2881 if self.accountId is None or not self.accountId: 2882 uLogger.error("Variable `accountId` must be defined for using this method!") 2883 raise Exception("Account ID required") 2884 2885 if operation is None or not operation or operation not in ("Buy", "Sell"): 2886 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2887 raise Exception("Incorrect value") 2888 2889 if lots is None or lots < 1: 2890 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2891 lots = 1 2892 2893 if tp is None or tp < 0: 2894 tp = 0 2895 2896 if sl is None or sl < 0: 2897 sl = 0 2898 2899 if expDate is None or not expDate: 2900 expDate = "Undefined" 2901 2902 if not (self.ticker or self.figi): 2903 uLogger.error("Ticker or FIGI must be defined!") 2904 raise Exception("Ticker or FIGI required") 2905 2906 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2907 self.ticker = instrument["ticker"] 2908 self.figi = instrument["figi"] 2909 2910 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2911 2912 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2913 self.body = str({ 2914 "figi": self.figi, 2915 "quantity": str(lots), 2916 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2917 "accountId": str(self.accountId), 2918 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2919 }) 2920 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2921 2922 if "orderId" in response.keys(): 2923 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2924 operation, response["orderId"], 2925 self.ticker, self.figi, lots, 2926 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2927 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2928 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2929 )) 2930 2931 else: 2932 uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.") 2933 2934 if tp > 0: 2935 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2936 2937 if sl > 0: 2938 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2939 2940 return response 2941 2942 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2943 """ 2944 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2945 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2946 2947 See also: `Order()` and `Trade()` docstrings. 2948 2949 :param lots: volume, integer count of lots >= 1. 2950 :param tp: float > 0, take profit price of stop-order. 2951 :param sl: float > 0, stop loss price of stop-order. 2952 :param expDate: it's a local date in future. 2953 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2954 :return: JSON with response from broker server. 2955 """ 2956 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2957 2958 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2959 """ 2960 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2961 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2962 2963 See also: `Order()` and `Trade()` docstrings. 2964 2965 :param lots: volume, integer count of lots >= 1. 2966 :param tp: float > 0, take profit price of stop-order. 2967 :param sl: float > 0, stop loss price of stop-order. 2968 :param expDate: it's a local date in the future. 2969 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2970 :return: JSON with response from broker server. 2971 """ 2972 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 2973 2974 def CloseTrades(self, tickers: list, portfolio: dict = None) -> None: 2975 """ 2976 Close position of given instruments. 2977 2978 :param tickers: tickers list of instruments that must be closed. 2979 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2980 This avoids unnecessary downloading data from the server. 2981 """ 2982 if not tickers: 2983 uLogger.info("Tickers list is empty, nothing to close.") 2984 2985 else: 2986 if portfolio is None or not portfolio: 2987 portfolio = self.Overview(show=False) 2988 2989 allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2990 uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers)) 2991 2992 for ticker in tickers: 2993 if ticker not in allOpenedTickers: 2994 uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker)) 2995 continue 2996 2997 # search open trade info about instrument by ticker: 2998 instrument = {} 2999 for iType in TKS_INSTRUMENTS: 3000 if instrument: 3001 break 3002 3003 for item in portfolio["stat"][iType]: 3004 if item["ticker"] == ticker: 3005 instrument = item 3006 break 3007 3008 if instrument: 3009 self.ticker = ticker 3010 self.figi = instrument["figi"] 3011 3012 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3013 self.ticker, 3014 self.figi, 3015 int(instrument["volume"]), 3016 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3017 )) 3018 3019 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3020 3021 if tradeLots > 0: 3022 if instrument["blocked"] > 0: 3023 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3024 instrument["blocked"], 3025 self.ticker, 3026 tradeLots, 3027 )) 3028 3029 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3030 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3031 3032 else: 3033 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3034 3035 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3036 """ 3037 Close all positions of given instruments with defined type. 3038 3039 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3040 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3041 This avoids unnecessary downloading data from the server. 3042 """ 3043 if iType not in TKS_INSTRUMENTS: 3044 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3045 3046 else: 3047 if portfolio is None or not portfolio: 3048 portfolio = self.Overview(show=False) 3049 3050 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3051 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3052 3053 if tickers and portfolio: 3054 self.CloseTrades(tickers, portfolio) 3055 3056 else: 3057 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3058 3059 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3060 """ 3061 Universal method to create market or limit orders with all available parameters for current `accountId`. 3062 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3063 3064 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3065 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3066 3067 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3068 then broker immediately open market order as you can do simple --buy or --sell operations! 3069 3070 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3071 When current price will go up or down to target price value then broker opens a limit order. 3072 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3073 3074 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3075 3076 :param operation: string "Buy" or "Sell". 3077 :param orderType: string "Limit" or "Stop". 3078 :param lots: volume, integer count of lots >= 1. 3079 :param targetPrice: target price > 0. This is open trade price for limit order. 3080 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3081 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3082 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3083 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3084 Stop loss order always executed by market price. 3085 :param expDate: string "Undefined" by default or local date in future. 3086 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3087 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3088 A limit order has no expiration date, it lasts until the end of the trading day. 3089 :return: JSON with response from broker server. 3090 """ 3091 if self.accountId is None or not self.accountId: 3092 uLogger.error("Variable `accountId` must be defined for using this method!") 3093 raise Exception("Account ID required") 3094 3095 if operation is None or not operation or operation not in ("Buy", "Sell"): 3096 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3097 raise Exception("Incorrect value") 3098 3099 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3100 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3101 raise Exception("Incorrect value") 3102 3103 if lots is None or lots < 1: 3104 uLogger.error("You must define trade volume > 0: integer count of lots!") 3105 raise Exception("Incorrect value") 3106 3107 if targetPrice is None or targetPrice <= 0: 3108 uLogger.error("Target price for limit-order must be greater than 0!") 3109 raise Exception("Incorrect value") 3110 3111 if limitPrice is None or limitPrice <= 0: 3112 limitPrice = targetPrice 3113 3114 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3115 stopType = "Limit" 3116 3117 if expDate is None or not expDate: 3118 expDate = "Undefined" 3119 3120 if not (self.ticker or self.figi): 3121 uLogger.error("Tocker or FIGI must be defined!") 3122 raise Exception("Ticker or FIGI required") 3123 3124 response = {} 3125 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3126 self.ticker = instrument["ticker"] 3127 self.figi = instrument["figi"] 3128 3129 if orderType == "Limit": 3130 uLogger.debug( 3131 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3132 self.ticker, self.figi, 3133 operation, lots, targetPrice, instrument["currency"], 3134 )) 3135 3136 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3137 self.body = str({ 3138 "figi": self.figi, 3139 "quantity": str(lots), 3140 "price": FloatToNano(targetPrice), 3141 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3142 "accountId": str(self.accountId), 3143 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3144 }) 3145 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3146 3147 if "orderId" in response.keys(): 3148 uLogger.info( 3149 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3150 response["orderId"], 3151 self.ticker, self.figi, 3152 operation, lots, targetPrice, instrument["currency"], 3153 )) 3154 3155 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3156 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3157 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3158 targetPrice, instrument["currency"], 3159 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3160 )) 3161 3162 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3163 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3164 targetPrice, instrument["currency"], 3165 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3166 )) 3167 3168 else: 3169 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3170 3171 if orderType == "Stop": 3172 uLogger.debug( 3173 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3174 self.ticker, self.figi, 3175 operation, lots, 3176 targetPrice, instrument["currency"], 3177 limitPrice, instrument["currency"], 3178 stopType, expDate, 3179 )) 3180 3181 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3182 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3183 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3184 3185 body = { 3186 "figi": self.figi, 3187 "quantity": str(lots), 3188 "price": FloatToNano(limitPrice), 3189 "stopPrice": FloatToNano(targetPrice), 3190 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3191 "accountId": str(self.accountId), 3192 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3193 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3194 } 3195 3196 if expDateUTC: 3197 body["expireDate"] = expDateUTC 3198 3199 self.body = str(body) 3200 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3201 3202 if "stopOrderId" in response.keys(): 3203 uLogger.info( 3204 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3205 response["stopOrderId"], 3206 self.ticker, self.figi, 3207 operation, lots, 3208 targetPrice, instrument["currency"], 3209 limitPrice, instrument["currency"], 3210 TKS_STOP_ORDER_TYPES[stopOrderType], 3211 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3212 )) 3213 3214 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3215 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3216 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3217 targetPrice, instrument["currency"], 3218 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3219 )) 3220 3221 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3222 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3223 targetPrice, instrument["currency"], 3224 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3225 )) 3226 3227 else: 3228 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3229 3230 return response 3231 3232 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3233 """ 3234 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3235 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3236 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3237 See also: `Order()` docstring. 3238 3239 :param lots: volume, integer count of lots >= 1. 3240 :param targetPrice: target price > 0. This is open trade price for limit order. 3241 :return: JSON with response from broker server. 3242 """ 3243 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3244 3245 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3246 """ 3247 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3248 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3249 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3250 target price value then broker opens a limit order. See also: `Order()` docstring. 3251 3252 :param lots: volume, integer count of lots >= 1. 3253 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3254 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3255 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3256 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3257 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3258 :param expDate: string "Undefined" by default or local date in future. 3259 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3260 This date is converting to UTC format for server. 3261 :return: JSON with response from broker server. 3262 """ 3263 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3264 3265 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3266 """ 3267 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3268 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3269 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3270 See also: `Order()` docstring. 3271 3272 :param lots: volume, integer count of lots >= 1. 3273 :param targetPrice: target price > 0. This is open trade price for limit order. 3274 :return: JSON with response from broker server. 3275 """ 3276 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3277 3278 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3279 """ 3280 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3281 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3282 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3283 target price value then broker opens a limit order. See also: `Order()` docstring. 3284 3285 :param lots: volume, integer count of lots >= 1. 3286 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3287 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3288 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3289 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3290 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3291 :param expDate: string "Undefined" by default or local date in future. 3292 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3293 This date is converting to UTC format for server. 3294 :return: JSON with response from broker server. 3295 """ 3296 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3297 3298 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3299 """ 3300 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3301 3302 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3303 :param allOrdersIDs: pre-received lists of all active pending orders. 3304 This avoids unnecessary downloading data from the server. 3305 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3306 """ 3307 if self.accountId is None or not self.accountId: 3308 uLogger.error("Variable `accountId` must be defined for using this method!") 3309 raise Exception("Account ID required") 3310 3311 if orderIDs: 3312 if allOrdersIDs is None or not allOrdersIDs: 3313 rawOrders = self.RequestPendingOrders() 3314 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3315 3316 if allStopOrdersIDs is None or not allStopOrdersIDs: 3317 rawStopOrders = self.RequestStopOrders() 3318 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3319 3320 for orderID in orderIDs: 3321 idInPendingOrders = orderID in allOrdersIDs 3322 idInStopOrders = orderID in allStopOrdersIDs 3323 3324 if not (idInPendingOrders or idInStopOrders): 3325 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3326 continue 3327 3328 else: 3329 if idInPendingOrders: 3330 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3331 3332 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3333 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3334 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3335 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3336 3337 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3338 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3339 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3340 3341 else: 3342 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3343 3344 elif idInStopOrders: 3345 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3346 3347 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3348 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3349 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3350 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3351 3352 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3353 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3354 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3355 3356 else: 3357 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3358 3359 else: 3360 continue 3361 3362 def CloseAllOrders(self) -> None: 3363 """ 3364 Gets a list of open pending and stop orders and cancel it all. 3365 """ 3366 rawOrders = self.RequestPendingOrders() 3367 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3368 lenOrders = len(allOrdersIDs) 3369 3370 rawStopOrders = self.RequestStopOrders() 3371 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3372 lenSOrders = len(allStopOrdersIDs) 3373 3374 if lenOrders > 0 or lenSOrders > 0: 3375 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3376 3377 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3378 3379 else: 3380 uLogger.info("Orders not found, nothing to cancel.") 3381 3382 def CloseAll(self, *args) -> None: 3383 """ 3384 Close all available (not blocked) opened trades and orders. 3385 3386 Also, you can select one or more keywords case-insensitive: 3387 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3388 3389 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3390 """ 3391 overview = self.Overview(show=False) # get all open trades info 3392 3393 if len(args) == 0: 3394 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3395 self.CloseAllOrders() # close all pending and stop orders 3396 3397 for iType in TKS_INSTRUMENTS: 3398 if iType != "Currencies": 3399 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3400 3401 else: 3402 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3403 lowerArgs = [x.lower() for x in args] 3404 3405 if "orders" in lowerArgs: 3406 self.CloseAllOrders() # close all pending and stop orders 3407 3408 for iType in TKS_INSTRUMENTS: 3409 if iType.lower() in lowerArgs and iType != "Currencies": 3410 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3411 3412 @staticmethod 3413 def ParseOrderParameters(operation, **inputParameters): 3414 """ 3415 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3416 3417 :param operation: string "Buy" or "Sell". 3418 :param inputParameters: this is dict of strings that looks like this 3419 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3420 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3421 "prices" key: one or more prices to open limit-orders 3422 Counts of values in lots and prices lists must be equals! 3423 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3424 """ 3425 # TODO: update order grid work with api v2 3426 pass 3427 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3428 # 3429 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3430 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3431 # raise Exception("Incorrect value") 3432 # 3433 # if "l" in inputParameters.keys(): 3434 # inputParameters["lots"] = inputParameters.pop("l") 3435 # 3436 # if "p" in inputParameters.keys(): 3437 # inputParameters["prices"] = inputParameters.pop("p") 3438 # 3439 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3440 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3441 # raise Exception("Incorrect value") 3442 # 3443 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3444 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3445 # 3446 # if len(lots) != len(prices): 3447 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3448 # raise Exception("Incorrect value") 3449 # 3450 # uLogger.debug("Extracted parameters for orders:") 3451 # uLogger.debug("lots = {}".format(lots)) 3452 # uLogger.debug("prices = {}".format(prices)) 3453 # 3454 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3455 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3456 # uLogger.debug("Order parameters: {}".format(result)) 3457 # 3458 # return result 3459 3460 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3461 """ 3462 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3463 3464 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3465 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3466 """ 3467 result = False 3468 msg = "Instrument not defined!" 3469 3470 if portfolio is None or not portfolio: 3471 portfolio = self.Overview(show=False) 3472 3473 if self.ticker: 3474 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3475 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3476 3477 for iType in TKS_INSTRUMENTS: 3478 for instrument in portfolio["stat"][iType]: 3479 if instrument["ticker"] == self.ticker: 3480 result = True 3481 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3482 break 3483 3484 elif self.figi: 3485 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3486 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3487 3488 for iType in TKS_INSTRUMENTS: 3489 for instrument in portfolio["stat"][iType]: 3490 if instrument["figi"] == self.figi: 3491 result = True 3492 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3493 break 3494 3495 else: 3496 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3497 3498 uLogger.debug(msg) 3499 3500 return result 3501 3502 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3503 """ 3504 Returns instrument is in the user's portfolio if it presents there. 3505 Instrument must be defined by `ticker` (highly priority) or `figi`. 3506 3507 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3508 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3509 """ 3510 result = None 3511 msg = "Instrument not defined!" 3512 3513 if portfolio is None or not portfolio: 3514 portfolio = self.Overview(show=False) 3515 3516 if self.ticker: 3517 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3518 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3519 3520 for iType in TKS_INSTRUMENTS: 3521 for instrument in portfolio["stat"][iType]: 3522 if instrument["ticker"] == self.ticker: 3523 result = instrument 3524 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3525 break 3526 3527 elif self.figi: 3528 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3529 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3530 3531 for iType in TKS_INSTRUMENTS: 3532 for instrument in portfolio["stat"][iType]: 3533 if instrument["figi"] == self.figi: 3534 result = instrument 3535 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3536 break 3537 3538 else: 3539 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3540 3541 uLogger.debug(msg) 3542 3543 return result 3544 3545 def RequestLimits(self) -> dict: 3546 """ 3547 Method for obtaining the available funds for withdrawal for current `accountId`. 3548 3549 See also: 3550 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3551 - `OverviewLimits()` method 3552 3553 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3554 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3555 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3556 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3557 """ 3558 if self.accountId is None or not self.accountId: 3559 uLogger.error("Variable `accountId` must be defined for using this method!") 3560 raise Exception("Account ID required") 3561 3562 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3563 3564 self.body = str({"accountId": self.accountId}) 3565 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3566 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3567 3568 uLogger.debug("Records about available funds for withdrawal successfully received") 3569 3570 return rawLimits 3571 3572 def OverviewLimits(self, show: bool = False) -> dict: 3573 """ 3574 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3575 3576 See also: `RequestLimits()`. 3577 3578 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3579 :return: dict with raw parsed data from server and some calculated statistics about it. 3580 """ 3581 if self.accountId is None or not self.accountId: 3582 uLogger.error("Variable `accountId` must be defined for using this method!") 3583 raise Exception("Account ID required") 3584 3585 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3586 3587 view = { 3588 "rawLimits": rawLimits, 3589 "limits": { # parsed data for every currency: 3590 "money": { # this is an array of portfolio currency positions 3591 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3592 }, 3593 "blocked": { # this is an array of blocked currency 3594 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3595 }, 3596 "blockedGuarantee": { # this is locked money under collateral for futures 3597 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3598 }, 3599 }, 3600 } 3601 3602 # --- Prepare text table with limits in human-readable format: 3603 if show: 3604 info = [ 3605 "# Withdrawal limits\n\n", 3606 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3607 "* **Account ID:** [{}]\n".format(self.accountId), 3608 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3609 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3610 ] 3611 3612 for curr in view["limits"]["money"].keys(): 3613 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3614 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3615 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3616 3617 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3618 "[{}]".format(curr), 3619 "{:.2f}".format(view["limits"]["money"][curr]), 3620 "{:.2f}".format(availableMoney), 3621 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3622 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3623 ) 3624 3625 if curr == "rub": 3626 info.insert(5, infoStr) # insert at first position in table and after headers 3627 3628 else: 3629 info.append(infoStr) 3630 3631 infoText = "".join(info) 3632 3633 uLogger.info(infoText) 3634 3635 if self.withdrawalLimitsFile: 3636 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3637 fH.write(infoText) 3638 3639 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3640 3641 return view 3642 3643 def RequestAccounts(self) -> dict: 3644 """ 3645 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3646 3647 See also: 3648 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3649 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3650 - `OverviewUserInfo()` method 3651 3652 :return: dict with raw data from server that contains accounts info. Example of dict: 3653 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3654 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3655 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3656 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3657 """ 3658 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3659 3660 self.body = str({}) 3661 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3662 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3663 3664 uLogger.debug("Records about available accounts successfully received") 3665 3666 return rawAccounts 3667 3668 def RequestUserInfo(self) -> dict: 3669 """ 3670 Method for requesting common user's information. 3671 3672 See also: 3673 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3674 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3675 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3676 - `OverviewUserInfo()` method 3677 3678 :return: dict with raw data from server that contains user's information. Example of dict: 3679 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3680 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3681 """ 3682 uLogger.debug("Requesting common user's information. Wait, please...") 3683 3684 self.body = str({}) 3685 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3686 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3687 3688 uLogger.debug("Records about current user successfully received") 3689 3690 return rawUserInfo 3691 3692 def RequestMarginStatus(self, accountId: str = None) -> dict: 3693 """ 3694 Method for requesting margin calculation for defined account ID. 3695 3696 See also: 3697 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3698 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3699 - `OverviewUserInfo()` method 3700 3701 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3702 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3703 Example of responses: 3704 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3705 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3706 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3707 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3708 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3709 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3710 """ 3711 if accountId is None or not accountId: 3712 if self.accountId is None or not self.accountId: 3713 uLogger.error("Variable `accountId` must be defined for using this method!") 3714 raise Exception("Account ID required") 3715 3716 else: 3717 accountId = self.accountId # use `self.accountId` (main ID) by default 3718 3719 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3720 3721 self.body = str({"accountId": accountId}) 3722 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3723 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3724 3725 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3726 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3727 rawMargin = {} 3728 3729 else: 3730 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3731 3732 return rawMargin 3733 3734 def RequestTariffLimits(self) -> dict: 3735 """ 3736 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3737 3738 See also: 3739 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3740 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3741 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3742 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3743 - `OverviewUserInfo()` method 3744 3745 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3746 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3747 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3748 """ 3749 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3750 3751 self.body = str({}) 3752 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3753 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3754 3755 uLogger.debug("Records with limits of current tariff successfully received") 3756 3757 return rawTariffLimits 3758 3759 def RequestBondCoupons(self, iJSON: dict) -> dict: 3760 """ 3761 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3762 then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". 3763 All dates are in UTC timezone. 3764 3765 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3766 Documentation: 3767 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3768 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3769 3770 See also: `ExtendBondsData()`. 3771 3772 :param iJSON: raw json data of a bond from broker server, example: `iJSON = self.iList["Bonds"][self.ticker]` 3773 If raw iJSON is not data of bond then server returns an error [400] with message: 3774 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3775 :return: dictionary with bond payment calendar. Response example: 3776 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3777 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3778 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3779 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3780 """ 3781 if iJSON["figi"] is None or not iJSON["figi"]: 3782 uLogger.error("FIGI must be defined for using this method!") 3783 raise Exception("FIGI required") 3784 3785 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3786 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3787 3788 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3789 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3790 self.figi, 3791 startDate, 3792 endDate, 3793 )) 3794 3795 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3796 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3797 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3798 3799 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3800 uLogger.warning("Instrument type is not bond!") 3801 3802 else: 3803 uLogger.debug("Records about bond payment calendar successfully received") 3804 3805 return calendar 3806 3807 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3808 """ 3809 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3810 pandas dataframe with more information about bonds: main info, current prices, bond payment calendar, 3811 coupon yields, current yields and some statistics etc. 3812 3813 WARNING! This is too long operation if a lot of bonds requested from broker server. 3814 3815 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3816 3817 :param instruments: list of strings with tickers or FIGIs. 3818 :param xlsx: if True then also exports pandas dataframe to xlsx-file `bondsXLSXFile`, default: `ext-bonds.xlsx`, 3819 for further used by data scientists or stock analytics. 3820 :return: wider pandas dataframe with more full and calculated data about bonds, than raw response from broker. 3821 In XLSX-file and pandas dataframe fields mean: 3822 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3823 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3824 """ 3825 if instruments is None or not instruments: 3826 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3827 raise Exception("Ticker or FIGI required") 3828 3829 if isinstance(instruments, str): 3830 instruments = [instruments] 3831 3832 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3833 3834 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3835 3836 iCount = len(uniqueInstruments) 3837 tooLong = iCount >= 20 3838 if tooLong: 3839 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3840 3841 bonds = None 3842 for i, self.figi in enumerate(uniqueInstruments): 3843 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3844 3845 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3846 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3847 rawBond = self.SearchByFIGI(requestPrice=True) 3848 3849 # Widen raw data with UTC current time (iData["actualDateTime"]): 3850 actualDate = datetime.now(tzutc()) 3851 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3852 3853 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3854 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3855 3856 # Replace some values with human-readable: 3857 iData["nominalCurrency"] = iData["nominal"]["currency"] 3858 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3859 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3860 iData["aciCurrency"] = iData["aciValue"]["currency"] 3861 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3862 iData["issueSize"] = int(iData["issueSize"]) 3863 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3864 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3865 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3866 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3867 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3868 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3869 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3870 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3871 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3872 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3873 3874 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3875 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3876 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3877 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3878 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3879 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3880 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3881 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3882 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3883 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3884 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3885 3886 # Widen raw data with calendar data from `rawCalendar` values: 3887 calendarData = [] 3888 for item in iData["rawCalendar"]["events"]: 3889 calendarData.append({ 3890 "couponDate": item["couponDate"], 3891 "couponNumber": int(item["couponNumber"]), 3892 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3893 "payCurrency": item["payOneBond"]["currency"], 3894 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3895 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3896 "couponStartDate": item["couponStartDate"], 3897 "couponEndDate": item["couponEndDate"], 3898 "couponPeriod": item["couponPeriod"], 3899 }) 3900 3901 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3902 if "maturityDate" not in iData.keys(): 3903 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3904 3905 # Widen raw data with Coupon Rate. 3906 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3907 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3908 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3909 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3910 3911 # Widen raw data with Yield to Maturity (YTM) on current date. 3912 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3913 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3914 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3915 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3916 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3917 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3918 3919 iData["calendar"] = calendarData # adds calendar at the end 3920 3921 # Remove not used data: 3922 iData.pop("uid") 3923 iData.pop("positionUid") 3924 iData.pop("currentPrice") 3925 iData.pop("rawCalendar") 3926 3927 colNames = list(iData.keys()) 3928 if bonds is None: 3929 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3930 3931 else: 3932 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3933 3934 else: 3935 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3936 3937 processed = round(100 * (i + 1) / iCount, 1) 3938 if tooLong and processed % 5 == 0: 3939 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3940 3941 else: 3942 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3943 3944 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3945 3946 # Saving bonds from pandas dataframe to XLSX sheet: 3947 if xlsx and self.bondsXLSXFile: 3948 with pd.ExcelWriter( 3949 path=self.bondsXLSXFile, 3950 date_format=TKS_DATE_FORMAT, 3951 datetime_format=TKS_DATE_TIME_FORMAT, 3952 mode="w", 3953 ) as writer: 3954 bonds.to_excel( 3955 writer, 3956 sheet_name="Extended bonds data", 3957 index=True, 3958 encoding="UTF-8", 3959 freeze_panes=(1, 1), 3960 ) # saving as XLSX-file with freeze first row and column as headers 3961 3962 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3963 3964 return bonds 3965 3966 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 3967 """ 3968 Creates bond payments calendar as pandas dataframe, and also save it to the XLSX-file, `calendar.xlsx` by default. 3969 3970 WARNING! This is too long operation if a lot of bonds requested from broker server. 3971 3972 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 3973 3974 :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains 3975 extended information about bonds: main info, current prices, bond payment calendar, 3976 coupon yields, current yields and some statistics etc. 3977 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 3978 :param xlsx: if True then also exports pandas dataframe to file `calendarFile` + `".xlsx"`, default: `calendar.xlsx`, 3979 for further used by data scientists or stock analytics. 3980 :return: pandas dataframe with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 3981 """ 3982 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 3983 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 3984 3985 uLogger.debug("Generating bond payments calendar data. Wait, please...") 3986 3987 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 3988 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 3989 calendar = None 3990 for bond in extBonds.iterrows(): 3991 for item in bond[1]["calendar"]: 3992 cData = { 3993 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 3994 "couponDate": item["couponDate"], 3995 "figi": bond[1]["figi"], 3996 "ticker": bond[1]["ticker"], 3997 "name": bond[1]["name"], 3998 "couponNumber": item["couponNumber"], 3999 "payOneBond": item["payOneBond"], 4000 "payCurrency": item["payCurrency"], 4001 "couponType": item["couponType"], 4002 "couponPeriod": item["couponPeriod"], 4003 "fixDate": item["fixDate"], 4004 "couponStartDate": item["couponStartDate"], 4005 "couponEndDate": item["couponEndDate"], 4006 } 4007 4008 if calendar is None: 4009 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4010 4011 else: 4012 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4013 4014 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4015 4016 # Saving calendar from pandas dataframe to XLSX sheet: 4017 if xlsx: 4018 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4019 4020 with pd.ExcelWriter( 4021 path=xlsxCalendarFile, 4022 date_format=TKS_DATE_FORMAT, 4023 datetime_format=TKS_DATE_TIME_FORMAT, 4024 mode="w", 4025 ) as writer: 4026 humanReadable = calendar.copy(deep=True) 4027 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4028 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4029 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4030 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4031 humanReadable.columns = colNames # human-readable column names 4032 4033 humanReadable.to_excel( 4034 writer, 4035 sheet_name="Bond payments calendar", 4036 index=False, 4037 encoding="UTF-8", 4038 freeze_panes=(1, 2), 4039 ) # saving as XLSX-file with freeze first row and column as headers 4040 4041 del humanReadable # release df in memory 4042 4043 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4044 4045 return calendar 4046 4047 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4048 """ 4049 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4050 Also, creates Markdown file with calendar data, `calendar.md` by default. 4051 4052 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4053 4054 :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains 4055 extended information about bonds: main info, current prices, bond payment calendar, 4056 coupon yields, current yields and some statistics etc. 4057 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4058 :param show: if `True` then also printing bonds payment calendar to the console, 4059 otherwise save to file `calendarFile` only. `False` by default. 4060 :return: multilines text in Markdown format with bonds payment calendar as a table. 4061 """ 4062 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4063 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4064 4065 infoText = "# Bond payments calendar\n\n" 4066 4067 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate pandas dataframe with full calendar data 4068 4069 if not calendar.empty: 4070 splitLine = "| | | | | | | | | |\n" 4071 4072 info = [ 4073 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4074 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4075 ] 4076 4077 newMonth = False 4078 notOneBond = calendar["figi"].nunique() > 1 4079 for i, bond in enumerate(calendar.iterrows()): 4080 if newMonth and notOneBond: 4081 info.append(splitLine) 4082 4083 info.append( 4084 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4085 " +" if bond[1]["paid"] else " —", 4086 bond[1]["couponDate"].split("T")[0], 4087 bond[1]["figi"], 4088 bond[1]["ticker"], 4089 bond[1]["couponNumber"], 4090 "{} {}".format( 4091 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4092 bond[1]["payCurrency"], 4093 ), 4094 bond[1]["couponType"], 4095 bond[1]["couponPeriod"], 4096 bond[1]["fixDate"].split("T")[0], 4097 ) 4098 ) 4099 4100 if i < len(calendar.values) - 1: 4101 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4102 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4103 newMonth = False if curDate.month == nextDate.month else True 4104 4105 else: 4106 newMonth = False 4107 4108 infoText += "".join(info) 4109 4110 if show: 4111 uLogger.info("{}".format(infoText)) 4112 4113 if self.calendarFile is not None: 4114 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4115 fH.write(infoText) 4116 4117 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4118 4119 else: 4120 infoText += "No data\n" 4121 4122 return infoText 4123 4124 def OverviewAccounts(self, show: bool = False) -> dict: 4125 """ 4126 Method for parsing and show simple table with all available user accounts. 4127 4128 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4129 4130 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4131 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4132 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4133 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4134 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4135 "closed": "—", "access": "Full access" }, ...}}` 4136 """ 4137 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4138 4139 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4140 accounts = { 4141 item["id"]: { 4142 "type": TKS_ACCOUNT_TYPES[item["type"]], 4143 "name": item["name"], 4144 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4145 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4146 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4147 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4148 } for item in rawAccounts["accounts"] 4149 } 4150 4151 # Raw and parsed data with some fields replaced in "stat" section: 4152 view = { 4153 "rawAccounts": rawAccounts, 4154 "stat": accounts, 4155 } 4156 4157 # --- Prepare simple text table with only accounts data in human-readable format: 4158 if show: 4159 info = [ 4160 "# User accounts\n\n", 4161 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4162 "| Account ID | Type | Status | Name |\n", 4163 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4164 ] 4165 4166 for account in view["stat"].keys(): 4167 info.extend([ 4168 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4169 account, 4170 view["stat"][account]["type"], 4171 view["stat"][account]["status"], 4172 view["stat"][account]["name"], 4173 ) 4174 ]) 4175 4176 infoText = "".join(info) 4177 4178 uLogger.info(infoText) 4179 4180 if self.userAccountsFile: 4181 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4182 fH.write(infoText) 4183 4184 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4185 4186 return view 4187 4188 def OverviewUserInfo(self, show: bool = False) -> dict: 4189 """ 4190 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4191 4192 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4193 4194 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4195 :return: dict with raw parsed data from server and some calculated statistics about it. 4196 """ 4197 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4198 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4199 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4200 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4201 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4202 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4203 4204 # This is dict with parsed common user data: 4205 userInfo = { 4206 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4207 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4208 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4209 "tariff": rawUserInfo["tariff"], 4210 } 4211 4212 # This is an array of dict with parsed margin statuses for every account IDs: 4213 margins = {} 4214 for accountId in accounts.keys(): 4215 if rawMargins[accountId]: 4216 margins[accountId] = { 4217 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4218 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4219 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4220 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4221 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4222 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4223 } 4224 4225 else: 4226 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4227 4228 unary = {} # unary-connection limits 4229 for item in rawTariffLimits["unaryLimits"]: 4230 if item["limitPerMinute"] in unary.keys(): 4231 unary[item["limitPerMinute"]].extend(item["methods"]) 4232 4233 else: 4234 unary[item["limitPerMinute"]] = item["methods"] 4235 4236 stream = {} # stream-connection limits 4237 for item in rawTariffLimits["streamLimits"]: 4238 if item["limit"] in stream.keys(): 4239 stream[item["limit"]].extend(item["streams"]) 4240 4241 else: 4242 stream[item["limit"]] = item["streams"] 4243 4244 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4245 limits = { 4246 "unary": unary, 4247 "stream": stream, 4248 } 4249 4250 # Raw and parsed data as an output result: 4251 view = { 4252 "rawUserInfo": rawUserInfo, 4253 "rawAccounts": rawAccounts, 4254 "rawMargins": rawMargins, 4255 "rawTariffLimits": rawTariffLimits, 4256 "stat": { 4257 "userInfo": userInfo, 4258 "accounts": accounts, 4259 "margins": margins, 4260 "limits": limits, 4261 }, 4262 } 4263 4264 # --- Prepare text table with user information in human-readable format: 4265 if show: 4266 info = [ 4267 "# Full user information\n\n", 4268 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4269 "## Common information\n\n", 4270 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4271 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4272 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4273 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4274 "\n## User accounts\n\n", 4275 ] 4276 4277 for account in view["stat"]["accounts"].keys(): 4278 info.extend([ 4279 "### ID: [{}]\n\n".format(account), 4280 "| Parameters | Values |\n", 4281 "|----------------------|--------------------------------------------------------------|\n", 4282 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4283 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4284 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4285 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4286 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4287 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4288 ]) 4289 4290 if margins[account]: 4291 info.extend([ 4292 "| Margin status: | Enabled |\n", 4293 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4294 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4295 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4296 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4297 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4298 ]) 4299 4300 else: 4301 info.append("| Margin status: | Disabled |\n\n") 4302 4303 info.extend([ 4304 "\n## Current user tariff limits\n", 4305 "\nSee also:\n", 4306 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4307 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4308 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4309 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4310 "\n### Unary limits\n", 4311 ]) 4312 4313 if unary: 4314 for key, values in sorted(unary.items()): 4315 info.append("\n* Max requests per minute: {}\n".format(key)) 4316 4317 for value in values: 4318 info.append(" - {}\n".format(value)) 4319 4320 else: 4321 info.append("\nNot available\n") 4322 4323 info.append("\n### Stream limits\n") 4324 4325 if stream: 4326 for key, values in sorted(stream.items()): 4327 info.append("\n* Max stream connections: {}\n".format(key)) 4328 4329 for value in values: 4330 info.append(" - {}\n".format(value)) 4331 4332 else: 4333 info.append("\nNot available\n") 4334 4335 infoText = "".join(info) 4336 4337 uLogger.info(infoText) 4338 4339 if self.userInfoFile: 4340 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4341 fH.write(infoText) 4342 4343 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4344 4345 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
196 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 197 """ 198 Main class init. 199 200 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 201 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 202 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 203 :param useCache: use default cache file with raw data to use instead of `iList`. 204 True by default. Cache is auto-update if new day has come. 205 If you don't want to use cache and always updates raw data then set `useCache=False`. 206 :param defaultCache: path to default cache file. `dump.json` by default. 207 """ 208 if token is None or not token: 209 try: 210 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 211 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 212 213 except KeyError: 214 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 215 raise Exception("Token required") 216 217 else: 218 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 219 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 220 221 if accountId is None or not accountId: 222 try: 223 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 224 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 225 226 except KeyError: 227 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 228 229 else: 230 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 231 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 232 233 self.version = __version__ # duplicate here used TKSBrokerAPI main version 234 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 235 236 Latest version: https://pypi.org/project/tksbrokerapi/ 237 """ 238 239 self.aliases = TKS_TICKER_ALIASES 240 """Some aliases instead official tickers. 241 242 See also: `TKSEnums.TKS_TICKER_ALIASES` 243 """ 244 245 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 246 247 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 248 249 self.ticker = "" 250 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 251 252 See also: `SearchByTicker()`, `SearchInstruments()`. 253 """ 254 255 self.figi = "" 256 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 257 258 See also: `SearchByFIGI()`, `SearchInstruments()`. 259 """ 260 261 self.depth = 1 262 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 263 264 See also: `GetCurrentPrices()`. 265 """ 266 267 self.server = r"https://invest-public-api.tinkoff.ru/rest" 268 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 269 270 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 271 """ 272 273 uLogger.debug("Broker API server: {}".format(self.server)) 274 275 self.timeout = 15 276 """Server operations timeout in seconds. Default: `15`. 277 278 See also: `SendAPIRequest()`. 279 """ 280 281 self.headers = { 282 "Content-Type": "application/json", 283 "accept": "application/json", 284 "Authorization": "Bearer {}".format(self.token), 285 "x-app-name": "Tim55667757.TKSBrokerAPI", 286 } 287 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 288 289 See also: `SendAPIRequest()`. 290 """ 291 292 self.body = None 293 """Request body which send to broker server. Default: `None`. 294 295 See also: `SendAPIRequest()`. 296 """ 297 298 self.historyFile = None 299 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only pandas dataframe. 300 301 See also: `History()`. 302 """ 303 304 self.htmlHistoryFile = "index.html" 305 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 306 307 See also: `ShowHistoryChart()`. 308 """ 309 310 self.instrumentsFile = "instruments.md" 311 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 312 313 See also: `ShowInstrumentsInfo()`. 314 """ 315 316 self.searchResultsFile = "search-results.md" 317 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 318 319 See also: `SearchInstruments()`. 320 """ 321 322 self.pricesFile = "prices.md" 323 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 324 325 See also: `GetListOfPrices()`. 326 """ 327 328 self.infoFile = "info.md" 329 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 330 331 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 332 """ 333 334 self.bondsXLSXFile = "ext-bonds.xlsx" 335 """Filename where wider pandas dataframe with more information about bonds: main info, current prices, 336 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 337 338 See also: `ExtendBondsData()`. 339 """ 340 341 self.calendarFile = "calendar.md" 342 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 343 344 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 345 346 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 347 """ 348 349 self.overviewFile = "overview.md" 350 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 351 352 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 353 """ 354 355 self.overviewDigestFile = "overview-digest.md" 356 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 357 358 See also: `Overview()` with parameter `details="digest"`. 359 """ 360 361 self.overviewPositionsFile = "overview-positions.md" 362 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 363 364 See also: `Overview()` with parameter `details="positions"`. 365 """ 366 367 self.overviewOrdersFile = "overview-orders.md" 368 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 369 370 See also: `Overview()` with parameter `details="orders"`. 371 """ 372 373 self.overviewAnalyticsFile = "overview-analytics.md" 374 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 375 376 See also: `Overview()` with parameter `details="analytics"`. 377 """ 378 379 self.reportFile = "deals.md" 380 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 381 382 See also: `Deals()`. 383 """ 384 385 self.withdrawalLimitsFile = "limits.md" 386 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 387 388 See also: `OverviewLimits()` and `RequestLimits()`. 389 """ 390 391 self.userInfoFile = "user-info.md" 392 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 393 394 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 395 """ 396 397 self.userAccountsFile = "accounts.md" 398 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 399 400 See also: `OverviewAccounts()`, `RequestAccounts()`. 401 """ 402 403 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 404 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 405 406 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 407 408 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 409 """ 410 411 self.iList = None # init iList for raw instruments data 412 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 413 414 See also: `Listing()`, `DumpInstruments()`. 415 """ 416 417 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 418 if useCache: 419 if os.path.exists(self.iListDumpFile): 420 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 421 curTime = datetime.now(tzutc()) 422 423 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 424 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 425 426 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 427 428 else: 429 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 430 431 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 432 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 433 434 else: 435 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 436 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 437 438 else: 439 self.iList = self.Listing() # request new raw instruments data from broker server 440 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 441 442 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 443 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 444 445 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 446 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
String with ticker, e.g. GOOGL. Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6.
See also: SearchByFIGI(), SearchInstruments().
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only pandas dataframe.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider pandas dataframe with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
470 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 471 """ 472 Send GET or POST request to broker server and receive JSON object. 473 474 self.header: must be defining with dictionary of headers. 475 self.body: if define then used as request body. None by default. 476 self.timeout: global request timeout, 15 seconds by default. 477 :param url: url with REST request. 478 :param reqType: send "GET" or "POST" request. "GET" by default. 479 :param retry: how many times retry after first request if an 5xx server errors occurred. 480 :param pause: sleep time in seconds between retries. 481 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 482 :return: response JSON (dictionary) from broker. 483 """ 484 if reqType not in ("GET", "POST"): 485 uLogger.error("You can define request type: 'GET' or 'POST'!") 486 raise Exception("Incorrect value") 487 488 if debug: 489 uLogger.debug("Request parameters:") 490 uLogger.debug(" - REST API URL: {}".format(url)) 491 uLogger.debug(" - request type: {}".format(reqType)) 492 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 493 uLogger.debug(" - body: {}".format(self.body)) 494 495 # fast hack to avoid all operations with some tickers/FIGI 496 responseJSON = {} 497 oK = True 498 for item in self.exclude: 499 if item in url: 500 if debug: 501 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 502 503 oK = False 504 break 505 506 if oK: 507 counter = 0 508 response = None 509 errMsg = "" 510 511 while not response and counter <= retry: 512 if reqType == "GET": 513 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 514 515 if reqType == "POST": 516 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 517 518 if debug: 519 uLogger.debug("Response:") 520 uLogger.debug(" - status code: {}".format(response.status_code)) 521 uLogger.debug(" - reason: {}".format(response.reason)) 522 uLogger.debug(" - body length: {}".format(len(response.text))) 523 uLogger.debug(" - headers: {}".format(response.headers)) 524 525 # Server returns some headers: 526 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 527 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 528 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 529 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 530 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 531 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 532 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 533 sleep(rateLimitWait) 534 535 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 536 if 400 <= response.status_code < 500: 537 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 538 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 539 counter = retry + 1 540 541 if 500 <= response.status_code < 600: 542 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 543 uLogger.debug(" - not oK, {}".format(errMsg)) 544 counter += 1 545 546 if counter <= retry: 547 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 548 sleep(pause) 549 550 responseJSON = self._ParseJSON(response.text) 551 552 if errMsg: 553 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 554 uLogger.error(" - not oK, {}".format(errMsg)) 555 556 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
- debug: if
Truethen print more debug information, e.g. request and response parameters, headers etc.
Returns
response JSON (dictionary) from broker.
589 def Listing(self) -> dict: 590 """ 591 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 592 593 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 594 """ 595 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 596 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 597 598 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 599 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 600 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 601 602 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 603 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 604 poolUpdater.close() 605 606 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 607 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 608 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 609 610 # calculate minimum price increment (step) for all instruments and set up instrument's type: 611 for iType in iList.keys(): 612 for ticker in iList[iType]: 613 iList[iType][ticker]["type"] = iType 614 615 if "minPriceIncrement" in iList[iType][ticker].keys(): 616 iList[iType][ticker]["step"] = NanoToFloat( 617 iList[iType][ticker]["minPriceIncrement"]["units"], 618 iList[iType][ticker]["minPriceIncrement"]["nano"], 619 ) 620 621 else: 622 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 623 624 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
626 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 627 """ 628 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 629 630 See also: `DumpInstruments()`, `Listing()`. 631 632 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 633 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 634 """ 635 if self.iListDumpFile is None or not self.iListDumpFile: 636 uLogger.error("Output name of dump file must be defined!") 637 raise Exception("Filename required") 638 639 if not self.iList or forceUpdate: 640 self.iList = self.Listing() 641 642 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 643 644 # Save as XLSX with separated sheets for every type of instruments: 645 with pd.ExcelWriter( 646 path=xlsxDumpFile, 647 date_format=TKS_DATE_FORMAT, 648 datetime_format=TKS_DATE_TIME_FORMAT, 649 mode="w", 650 ) as writer: 651 for iType in TKS_INSTRUMENTS: 652 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 653 df = df[sorted(df)] # sorted by column names 654 df = df.applymap( 655 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 656 na_action="ignore", 657 ) # converting numbers from nano-type to float in every cell 658 df.to_excel( 659 writer, 660 sheet_name=iType, 661 encoding="UTF-8", 662 freeze_panes=(1, 1), 663 ) # saving as XLSX-file with freeze first row and column as headers 664 665 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
667 def DumpInstruments(self, forceUpdate: bool = True) -> str: 668 """ 669 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 670 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 671 672 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 673 674 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 675 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 676 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 677 """ 678 if self.iListDumpFile is None or not self.iListDumpFile: 679 uLogger.error("Output name of dump file must be defined!") 680 raise Exception("Filename required") 681 682 if not self.iList or forceUpdate: 683 self.iList = self.Listing() 684 685 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 686 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 687 fH.write(jsonDump) 688 689 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 690 691 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
693 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 694 """ 695 Show information about one instrument defined by json data and prints it in Markdown format. 696 697 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 698 699 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 700 :param show: if `True` then also printing information about instrument and its current price. 701 :return: multilines text in Markdown format with information about one instrument. 702 """ 703 splitLine = "| | |\n" 704 infoText = "" 705 706 if iJSON is not None and iJSON and isinstance(iJSON, dict): 707 info = [ 708 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 709 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 710 "| Parameters | Values |\n", 711 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 712 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 713 "| Full name: | {:<54} |\n".format(iJSON["name"]), 714 ] 715 716 if "sector" in iJSON.keys() and iJSON["sector"]: 717 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 718 719 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 720 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 721 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 722 ))) 723 724 info.extend([ 725 splitLine, 726 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 727 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 728 ]) 729 730 if "isin" in iJSON.keys() and iJSON["isin"]: 731 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 732 733 if "classCode" in iJSON.keys(): 734 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 735 736 info.extend([ 737 splitLine, 738 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 739 splitLine, 740 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 741 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 742 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 743 ]) 744 745 if iJSON["figi"]: 746 self.figi = iJSON["figi"] 747 iJSON = iJSON | self.RequestTradingStatus() 748 749 info.extend([ 750 splitLine, 751 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 752 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 753 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 754 ]) 755 756 info.append(splitLine) 757 758 if "type" in iJSON.keys() and iJSON["type"]: 759 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 760 761 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 762 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 763 764 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 765 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 766 767 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 768 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 769 770 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 771 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 772 773 if "focusType" in iJSON.keys() and iJSON["focusType"]: 774 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 775 776 if "assetType" in iJSON.keys() and iJSON["assetType"]: 777 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 778 779 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 780 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 781 782 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 783 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 784 785 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 786 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 787 788 if "currency" in iJSON.keys(): 789 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 790 791 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 792 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 793 794 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 795 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 796 797 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 798 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 799 800 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 801 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 802 803 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 804 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 805 806 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 807 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 808 809 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 810 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 811 812 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 813 info.append("| Perpetual bond: | Yes |\n") 814 815 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 816 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 817 818 iExt = None 819 if iJSON["type"] == "Bonds": 820 info.extend([ 821 splitLine, 822 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 823 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 824 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 825 iJSON["nominal"]["currency"], 826 )), 827 ]) 828 829 if "floatingCouponFlag" in iJSON.keys(): 830 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 831 832 if "amortizationFlag" in iJSON.keys(): 833 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 834 835 info.append(splitLine) 836 837 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 838 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 839 840 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 841 842 info.extend([ 843 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 844 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 845 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 846 ]) 847 848 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 849 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 850 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 851 iJSON["aciValue"]["currency"] 852 ))) 853 854 if "currentPrice" in iJSON.keys(): 855 info.append(splitLine) 856 857 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 858 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 859 860 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 861 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 862 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 863 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 864 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 865 866 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 867 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 868 869 info.extend([ 870 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 871 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 872 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 873 )), 874 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 875 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 876 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 877 )), 878 "| Changes between last deal price and last close | {:<54} |\n".format( 879 "{:.2f}%{}".format( 880 iJSON["currentPrice"]["changes"], 881 " ({}{:.2f} {})".format( 882 "+" if bondChangesDelta > 0 else "", 883 bondChangesDelta, 884 aciCurrency 885 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 886 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 887 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 888 currency 889 ), 890 ) 891 ), 892 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 893 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 894 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 895 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 896 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 897 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 898 )), 899 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 900 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 901 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 902 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 903 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 904 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 905 )), 906 ]) 907 908 if "lot" in iJSON.keys(): 909 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 910 911 if "step" in iJSON.keys() and iJSON["step"] != 0: 912 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 913 914 # Add bond payment calendar: 915 if iJSON["type"] == "Bonds": 916 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 917 info.extend(["\n", strCalendar]) 918 919 infoText += "".join(info) 920 921 if show: 922 uLogger.info("{}".format(infoText)) 923 924 else: 925 uLogger.debug("{}".format(infoText)) 926 927 if self.infoFile is not None: 928 with open(self.infoFile, "w", encoding="UTF-8") as fH: 929 fH.write(infoText) 930 931 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 932 933 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self.ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
935 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 936 """ 937 Search and return raw broker's information about instrument by its ticker. 938 `ticker` must be defined! If debug=True then print all debug messages. 939 940 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 941 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 942 :param debug: if `True` then print all debug console messages. 943 :return: JSON formatted data with information about instrument. 944 """ 945 tickerJSON = {} 946 if debug: 947 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 948 949 if not self.ticker: 950 uLogger.warning("self.ticker variable is not be empty!") 951 952 else: 953 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 954 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 955 raise Exception("Instrument not allowed") 956 957 if not self.iList: 958 self.iList = self.Listing() 959 960 if self.ticker in self.iList["Shares"].keys(): 961 tickerJSON = self.iList["Shares"][self.ticker] 962 if debug: 963 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 964 965 elif self.ticker in self.iList["Currencies"].keys(): 966 tickerJSON = self.iList["Currencies"][self.ticker] 967 if debug: 968 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 969 970 elif self.ticker in self.iList["Bonds"].keys(): 971 tickerJSON = self.iList["Bonds"][self.ticker] 972 if debug: 973 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 974 975 elif self.ticker in self.iList["Etfs"].keys(): 976 tickerJSON = self.iList["Etfs"][self.ticker] 977 if debug: 978 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 979 980 elif self.ticker in self.iList["Futures"].keys(): 981 tickerJSON = self.iList["Futures"][self.ticker] 982 if debug: 983 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 984 985 if tickerJSON: 986 self.figi = tickerJSON["figi"] 987 988 if requestPrice: 989 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 990 991 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 992 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 993 994 else: 995 tickerJSON["currentPrice"]["changes"] = 0 996 997 if show: 998 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 999 1000 else: 1001 if show: 1002 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1003 1004 return tickerJSON
Search and return raw broker's information about instrument by its ticker.
ticker must be defined! If debug=True then print all debug messages.
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console. - debug: if
Truethen print all debug console messages.
Returns
JSON formatted data with information about instrument.
1006 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1007 """ 1008 Search and return raw broker's information about instrument by its FIGI. 1009 `figi` must be defined! If debug=True then print all debug messages. 1010 1011 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1012 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1013 :param debug: if `True` then print all debug console messages. 1014 :return: JSON formatted data with information about instrument. 1015 """ 1016 figiJSON = {} 1017 if debug: 1018 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1019 1020 if not self.figi: 1021 uLogger.warning("self.figi variable is not be empty!") 1022 1023 else: 1024 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1025 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1026 raise Exception("Instrument not allowed") 1027 1028 if not self.iList: 1029 self.iList = self.Listing() 1030 1031 for item in self.iList["Shares"].keys(): 1032 if self.figi == self.iList["Shares"][item]["figi"]: 1033 figiJSON = self.iList["Shares"][item] 1034 1035 if debug: 1036 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1037 1038 break 1039 1040 if not figiJSON: 1041 for item in self.iList["Currencies"].keys(): 1042 if self.figi == self.iList["Currencies"][item]["figi"]: 1043 figiJSON = self.iList["Currencies"][item] 1044 1045 if debug: 1046 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1047 1048 break 1049 1050 if not figiJSON: 1051 for item in self.iList["Bonds"].keys(): 1052 if self.figi == self.iList["Bonds"][item]["figi"]: 1053 figiJSON = self.iList["Bonds"][item] 1054 1055 if debug: 1056 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1057 1058 break 1059 1060 if not figiJSON: 1061 for item in self.iList["Etfs"].keys(): 1062 if self.figi == self.iList["Etfs"][item]["figi"]: 1063 figiJSON = self.iList["Etfs"][item] 1064 1065 if debug: 1066 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1067 1068 break 1069 1070 if not figiJSON: 1071 for item in self.iList["Futures"].keys(): 1072 if self.figi == self.iList["Futures"][item]["figi"]: 1073 figiJSON = self.iList["Futures"][item] 1074 1075 if debug: 1076 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1077 1078 break 1079 1080 if figiJSON: 1081 self.figi = figiJSON["figi"] 1082 self.ticker = figiJSON["ticker"] 1083 1084 if requestPrice: 1085 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1086 1087 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1088 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1089 1090 else: 1091 figiJSON["currentPrice"]["changes"] = 0 1092 1093 if show: 1094 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1095 1096 else: 1097 if show: 1098 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1099 1100 return figiJSON
Search and return raw broker's information about instrument by its FIGI.
figi must be defined! If debug=True then print all debug messages.
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console. - debug: if
Truethen print all debug console messages.
Returns
JSON formatted data with information about instrument.
1102 def GetCurrentPrices(self, show: bool = True) -> dict: 1103 """ 1104 Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record: 1105 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1106 1107 See also: 1108 1109 :param show: if `True` then print DOM to log and console. 1110 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1111 """ 1112 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1113 1114 if self.depth < 1: 1115 uLogger.error("Depth of Market (DOM) must be >=1!") 1116 raise Exception("Incorrect value") 1117 1118 if not (self.ticker or self.figi): 1119 uLogger.error("self.ticker or self.figi variables must be defined!") 1120 raise Exception("Ticker or FIGI required") 1121 1122 if self.ticker and not self.figi: 1123 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1124 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1125 1126 if not self.ticker and self.figi: 1127 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1128 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1129 1130 if not self.figi: 1131 uLogger.error("FIGI is not defined!") 1132 raise Exception("Ticker or FIGI required") 1133 1134 else: 1135 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1136 1137 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1138 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1139 self.body = str({"figi": self.figi, "depth": self.depth}) 1140 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") 1141 1142 if pricesResponse: 1143 # list of dicts with sellers orders: 1144 prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1145 1146 # list of dicts with buyers orders: 1147 prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1148 1149 # max price of instrument at this time: 1150 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1151 1152 # min price of instrument at this time: 1153 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1154 1155 # last price of deal with instrument: 1156 prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0 1157 1158 # last close price of instrument: 1159 prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0 1160 1161 else: 1162 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1163 uLogger.debug("Server response: {}".format(pricesResponse)) 1164 1165 if show: 1166 if prices["buy"] or prices["sell"]: 1167 info = [ 1168 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1169 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1170 self.ticker, 1171 self.figi, 1172 self.depth, 1173 ), 1174 uLog.sepShort, "\n", 1175 " Orders of Buyers | Orders of Sellers\n", 1176 uLog.sepShort, "\n", 1177 " Sell prices (vol.) | Buy prices (vol.)\n", 1178 uLog.sepShort, "\n", 1179 ] 1180 1181 if not prices["buy"]: 1182 info.append(" | No orders!\n") 1183 sumBuy = 0 1184 1185 else: 1186 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1187 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1188 for item in maxMinSorted: 1189 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1190 1191 if not prices["sell"]: 1192 info.append("No orders! |\n") 1193 sumSell = 0 1194 1195 else: 1196 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1197 for item in prices["sell"]: 1198 info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1199 1200 info.extend([ 1201 uLog.sepShort, "\n", 1202 "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1203 uLog.sepShort, "\n", 1204 ]) 1205 1206 infoText = "".join(info) 1207 1208 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1209 1210 else: 1211 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1212 1213 return prices
Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record:
{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
See also:
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}.
1215 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1216 """ 1217 This method get and show information about all available broker instruments for current user account. 1218 If `instrumentsFile` string is not empty then also save information to this file. 1219 1220 :param show: if `True` then print results to console, if `False` - print only to file. 1221 :return: multi-lines string with all available broker instruments 1222 """ 1223 if not self.iList: 1224 self.iList = self.Listing() 1225 1226 info = [ 1227 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1228 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1229 ] 1230 1231 # add instruments count by type: 1232 for iType in self.iList.keys(): 1233 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1234 1235 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1236 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1237 1238 # generating info tables with all instruments by type: 1239 for iType in self.iList.keys(): 1240 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1241 1242 for instrument in self.iList[iType].keys(): 1243 iName = self.iList[iType][instrument]["name"] # instrument's name 1244 if len(iName) > 57: 1245 iName = "{}...".format(iName[:54]) # right trim for a long string 1246 1247 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1248 self.iList[iType][instrument]["ticker"], 1249 iName, 1250 self.iList[iType][instrument]["figi"], 1251 self.iList[iType][instrument]["currency"], 1252 self.iList[iType][instrument]["lot"], 1253 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1254 )) 1255 1256 infoText = "".join(info) 1257 1258 if show: 1259 uLogger.info(infoText) 1260 1261 if self.instrumentsFile: 1262 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1263 fH.write(infoText) 1264 1265 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1266 1267 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse- print only to file.
Returns
multi-lines string with all available broker instruments
1269 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1270 """ 1271 This method search and show information about instruments by part of its ticker, FIGI or name. 1272 If `searchResultsFile` string is not empty then also save information to this file. 1273 1274 :param pattern: string with part of ticker, FIGI or instrument's name. 1275 :param show: if `True` then print results to console, if `False` - return list of result only. 1276 :return: list of dictionaries with all found instruments. 1277 """ 1278 if not self.iList: 1279 self.iList = self.Listing() 1280 1281 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1282 compiledPattern = re.compile(pattern, re.IGNORECASE) 1283 1284 for iType in self.iList: 1285 for instrument in self.iList[iType].values(): 1286 searchResult = compiledPattern.search(" ".join( 1287 [instrument["ticker"], instrument["figi"], instrument["name"]] 1288 )) 1289 1290 if searchResult: 1291 searchResults[iType][instrument["ticker"]] = instrument 1292 1293 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1294 info = [ 1295 "# Search results\n\n", 1296 "* **Search pattern:** [{}]\n".format(pattern), 1297 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1298 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1299 ] 1300 infoShort = info[:] 1301 1302 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1303 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1304 skippedLine = "| ... | ... | ... | ... |\n" 1305 1306 if resultsLen == 0: 1307 info.append("\nNo results\n") 1308 infoShort.append("\nNo results\n") 1309 uLogger.warning("No results. Try changing your search pattern.") 1310 1311 else: 1312 for iType in searchResults: 1313 iTypeValuesCount = len(searchResults[iType].values()) 1314 if iTypeValuesCount > 0: 1315 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1316 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1317 1318 for instrument in searchResults[iType].values(): 1319 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1320 instrument["type"], 1321 instrument["ticker"], 1322 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1323 instrument["figi"], 1324 )) 1325 1326 if iTypeValuesCount <= 5: 1327 infoShort.extend(info[-iTypeValuesCount:]) 1328 1329 else: 1330 infoShort.extend(info[-5:]) 1331 infoShort.append(skippedLine) 1332 1333 infoText = "".join(info) 1334 infoTextShort = "".join(infoShort) 1335 1336 if show: 1337 uLogger.info(infoTextShort) 1338 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1339 1340 if self.searchResultsFile: 1341 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1342 fH.write(infoText) 1343 1344 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1345 1346 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse- return list of result only.
Returns
list of dictionaries with all found instruments.
1348 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1349 """ 1350 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1351 1352 :param instruments: list of strings with tickers or FIGIs. 1353 :return: list with unique instrument FIGIs only. 1354 """ 1355 requestedInstruments = [] 1356 for iName in instruments: 1357 if iName not in self.aliases.keys(): 1358 if iName not in requestedInstruments: 1359 requestedInstruments.append(iName) 1360 1361 else: 1362 if iName not in requestedInstruments: 1363 if self.aliases[iName] not in requestedInstruments: 1364 requestedInstruments.append(self.aliases[iName]) 1365 1366 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1367 1368 onlyUniqueFIGIs = [] 1369 for iName in requestedInstruments: 1370 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1371 continue 1372 1373 self.ticker = iName 1374 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1375 1376 if not iData: 1377 self.ticker = "" 1378 self.figi = iName 1379 1380 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1381 1382 if not iData: 1383 self.figi = "" 1384 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1385 1386 if iData and iData["figi"] not in onlyUniqueFIGIs: 1387 onlyUniqueFIGIs.append(iData["figi"]) 1388 1389 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1390 1391 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1393 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1394 """ 1395 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1396 See limits: https://tinkoff.github.io/investAPI/limits/ 1397 If `pricesFile` string is not empty then also save information to this file. 1398 1399 :param instruments: list of strings with tickers or FIGIs. 1400 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1401 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1402 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1403 """ 1404 if instruments is None or not instruments: 1405 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1406 raise Exception("Ticker or FIGI required") 1407 1408 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1409 1410 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1411 1412 iList = [] # trying to get info and current prices about all unique instruments: 1413 for self.figi in onlyUniqueFIGIs: 1414 iData = self.SearchByFIGI(requestPrice=True) 1415 iList.append(iData) 1416 1417 self.ShowListOfPrices(iList, show) 1418 1419 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse- prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1421 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1422 """ 1423 Show table contains current prices of given instruments. 1424 1425 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1426 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1427 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1428 :return: multilines text in Markdown format as a table contains current prices. 1429 """ 1430 infoText = "" 1431 1432 if show or self.pricesFile: 1433 info = [ 1434 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1435 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1436 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1437 ] 1438 1439 for item in iList: 1440 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1441 item["ticker"], 1442 item["figi"], 1443 item["type"], 1444 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1445 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1446 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1447 "{} / {}".format( 1448 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1449 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1450 ), 1451 "{} / {}".format( 1452 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1453 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1454 ), 1455 item["currency"], 1456 )) 1457 1458 infoText = "".join(info) 1459 1460 if show: 1461 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1462 1463 if self.pricesFile: 1464 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1465 fH.write(infoText) 1466 1467 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1468 1469 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse- prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1471 def RequestTradingStatus(self) -> dict: 1472 """ 1473 Requesting trading status for the instrument defined by `figi` variable. 1474 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1475 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1476 1477 :return: dictionary with trading status attributes. Response example: 1478 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1479 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1480 """ 1481 if self.figi is None or not self.figi: 1482 uLogger.error("Variable `figi` must be defined for using this method!") 1483 raise Exception("FIGI required") 1484 1485 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1486 1487 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1488 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1489 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1490 1491 uLogger.debug("Records about current trading status successfully received") 1492 1493 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1495 def RequestPortfolio(self) -> dict: 1496 """ 1497 Requesting actual user's portfolio for current `accountId`. 1498 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1499 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1500 1501 :return: dictionary with user's portfolio. 1502 """ 1503 if self.accountId is None or not self.accountId: 1504 uLogger.error("Variable `accountId` must be defined for using this method!") 1505 raise Exception("Account ID required") 1506 1507 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1508 1509 self.body = str({"accountId": self.accountId}) 1510 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1511 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1512 1513 uLogger.debug("Records about user's portfolio successfully received") 1514 1515 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1517 def RequestPositions(self) -> dict: 1518 """ 1519 Requesting open positions by currencies and instruments for current `accountId`. 1520 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1521 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1522 1523 :return: dictionary with open positions by instruments. 1524 """ 1525 if self.accountId is None or not self.accountId: 1526 uLogger.error("Variable `accountId` must be defined for using this method!") 1527 raise Exception("Account ID required") 1528 1529 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1530 1531 self.body = str({"accountId": self.accountId}) 1532 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1533 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1534 1535 uLogger.debug("Records about current open positions successfully received") 1536 1537 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1539 def RequestPendingOrders(self) -> list: 1540 """ 1541 Requesting current actual pending orders for current `accountId`. 1542 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1543 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1544 1545 :return: list of dictionaries with pending orders. 1546 """ 1547 if self.accountId is None or not self.accountId: 1548 uLogger.error("Variable `accountId` must be defined for using this method!") 1549 raise Exception("Account ID required") 1550 1551 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1552 1553 self.body = str({"accountId": self.accountId}) 1554 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1555 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1556 1557 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1558 1559 return rawOrders
Requesting current actual pending orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending orders.
1561 def RequestStopOrders(self) -> list: 1562 """ 1563 Requesting current actual stop orders for current `accountId`. 1564 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1565 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1566 1567 :return: list of dictionaries with stop orders. 1568 """ 1569 if self.accountId is None or not self.accountId: 1570 uLogger.error("Variable `accountId` must be defined for using this method!") 1571 raise Exception("Account ID required") 1572 1573 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1574 1575 self.body = str({"accountId": self.accountId}) 1576 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1577 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1578 1579 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1580 1581 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1583 def Overview(self, show: bool = False, details: str = "full") -> dict: 1584 """ 1585 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1586 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1587 are defined then also save information to file. 1588 1589 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1590 many requests about the state of the portfolio, and then, based on the received data, a large number 1591 of calculation and statistics are collected. 1592 1593 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1594 :param details: how detailed should the information be? You should specify one of strings: 1595 `full` - shows full available information about portfolio status (by default), 1596 `positions` - shows only open positions, 1597 `digest` - show a short digest of the portfolio status, 1598 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1599 `orders` - shows only sections of open limits and stop orders. 1600 :return: dictionary with client's raw portfolio and some statistics. 1601 """ 1602 if self.accountId is None or not self.accountId: 1603 uLogger.error("Variable `accountId` must be defined for using this method!") 1604 raise Exception("Account ID required") 1605 1606 view = { 1607 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1608 "headers": {}, # list of dictionaries, response headers without "positions" section 1609 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1610 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1611 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1612 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1613 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1614 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1615 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1616 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1617 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1618 }, 1619 "stat": { # --- some statistics calculated using "raw" sections: 1620 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1621 "availableRUB": 0., # available rubles (without other currencies) 1622 "blockedRUB": 0., # blocked sum in Russian Rouble 1623 "totalChangesRUB": 0., # changes for all open trades in RUB 1624 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1625 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1626 "sharesCostRUB": 0., # costs of all shares in RUB 1627 "bondsCostRUB": 0., # costs of all bonds in RUB 1628 "etfsCostRUB": 0., # costs of all etfs in RUB 1629 "futuresCostRUB": 0., # costs of all futures in RUB 1630 "Currencies": [], # list of dictionaries of all currencies statistics 1631 "Shares": [], # list of dictionaries of all shares statistics 1632 "Bonds": [], # list of dictionaries of all bonds statistics 1633 "Etfs": [], # list of dictionaries of all etfs statistics 1634 "Futures": [], # list of dictionaries of all futures statistics 1635 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1636 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1637 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1638 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1639 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1640 }, 1641 "analytics": { # --- some analytics of portfolio: 1642 "distrByAssets": {}, # portfolio distribution by assets 1643 "distrByCompanies": {}, # portfolio distribution by companies 1644 "distrBySectors": {}, # portfolio distribution by sectors 1645 "distrByCurrencies": {}, # portfolio distribution by currencies 1646 "distrByCountries": {}, # portfolio distribution by countries 1647 } 1648 } 1649 1650 details = details.lower() 1651 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1652 if details not in availableDetails: 1653 details = "full" 1654 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1655 1656 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1657 1658 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1659 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1660 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1661 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1662 1663 # save response headers without "positions" section: 1664 for key in portfolioResponse.keys(): 1665 if key != "positions": 1666 view["raw"]["headers"][key] = portfolioResponse[key] 1667 1668 else: 1669 continue 1670 1671 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1672 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1673 for item in portfolioResponse["positions"]: 1674 if item["instrumentType"] == "currency": 1675 self.figi = item["figi"] 1676 curr = self.SearchByFIGI(requestPrice=False) 1677 1678 # current price of currency in RUB: 1679 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1680 "name": curr["name"], 1681 "currentPrice": NanoToFloat( 1682 item["currentPrice"]["units"], 1683 item["currentPrice"]["nano"] 1684 ), 1685 } 1686 1687 view["raw"]["Currencies"].append(item) 1688 1689 elif item["instrumentType"] == "share": 1690 view["raw"]["Shares"].append(item) 1691 1692 elif item["instrumentType"] == "bond": 1693 view["raw"]["Bonds"].append(item) 1694 1695 elif item["instrumentType"] == "etf": 1696 view["raw"]["Etfs"].append(item) 1697 1698 elif item["instrumentType"] == "futures": 1699 view["raw"]["Futures"].append(item) 1700 1701 else: 1702 continue 1703 1704 # how many volume of currencies (by ISO currency name) are blocked: 1705 for item in view["raw"]["positions"]["blocked"]: 1706 blocked = NanoToFloat(item["units"], item["nano"]) 1707 if blocked > 0: 1708 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1709 1710 # how many volume of instruments (by FIGI) are blocked: 1711 for item in view["raw"]["positions"]["securities"]: 1712 blocked = int(item["blocked"]) 1713 if blocked > 0: 1714 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1715 1716 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1717 1718 if "rub" in allBlocked.keys(): 1719 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1720 1721 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1722 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1723 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1724 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1725 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1726 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1727 view["stat"]["portfolioCostRUB"] = sum([ 1728 view["stat"]["allCurrenciesCostRUB"], 1729 view["stat"]["sharesCostRUB"], 1730 view["stat"]["bondsCostRUB"], 1731 view["stat"]["etfsCostRUB"], 1732 view["stat"]["futuresCostRUB"], 1733 ]) 1734 1735 # --- calculating some portfolio statistics: 1736 byComp = {} # distribution by companies 1737 bySect = {} # distribution by sectors 1738 byCurr = {} # distribution by currencies (include RUB) 1739 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1740 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1741 1742 for item in portfolioResponse["positions"]: 1743 self.figi = item["figi"] 1744 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1745 1746 if instrument: 1747 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1748 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1749 1750 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1751 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1752 1753 else: 1754 blocked = 0 1755 1756 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1757 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1758 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1759 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1760 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1761 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1762 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1763 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1764 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1765 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1766 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1767 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1768 1769 statData = { 1770 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1771 "ticker": instrument["ticker"], # ticker by FIGI 1772 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1773 "volume": volume, # available volume of instrument 1774 "lots": lots, # volume in lots of instrument 1775 "direction": direction, # direction of an instrument's position: short or long 1776 "blocked": blocked, # blocked volume of currency or instrument 1777 "currentPrice": curPrice, # current instrument's price in basic asset 1778 "average": average, # current average position price 1779 "cost": cost, # current cost of all volume of instrument in basic asset 1780 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1781 "costRUB": costRUB, # cost of instrument in ruble 1782 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1783 "profit": profit, # expected profit at current moment 1784 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1785 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1786 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1787 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1788 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1789 "step": instrument["step"], # minimum price increment 1790 } 1791 1792 # adding distribution by unique countries: 1793 if statData["country"] not in byCountry.keys(): 1794 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1795 1796 else: 1797 byCountry[statData["country"]]["cost"] += costRUB 1798 byCountry[statData["country"]]["percent"] += percentCostRUB 1799 1800 if item["instrumentType"] != "currency": 1801 # adding distribution by unique companies: 1802 if statData["name"]: 1803 if statData["name"] not in byComp.keys(): 1804 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1805 1806 else: 1807 byComp[statData["name"]]["cost"] += costRUB 1808 byComp[statData["name"]]["percent"] += percentCostRUB 1809 1810 # adding distribution by unique sectors: 1811 if statData["sector"] not in bySect.keys(): 1812 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1813 1814 else: 1815 bySect[statData["sector"]]["cost"] += costRUB 1816 bySect[statData["sector"]]["percent"] += percentCostRUB 1817 1818 # adding distribution by unique currencies: 1819 if currency not in byCurr.keys(): 1820 byCurr[currency] = { 1821 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1822 "cost": costRUB, 1823 "percent": percentCostRUB 1824 } 1825 1826 else: 1827 byCurr[currency]["cost"] += costRUB 1828 byCurr[currency]["percent"] += percentCostRUB 1829 1830 # saving statistics for every instrument: 1831 if item["instrumentType"] == "currency": 1832 view["stat"]["Currencies"].append(statData) 1833 1834 # update dict with free funds for trading (total - blocked) by currencies 1835 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1836 view["stat"]["funds"][currency] = { 1837 "total": volume, 1838 "totalCostRUB": costRUB, # total volume cost in rubles 1839 "free": volume - blocked, 1840 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1841 } 1842 1843 elif item["instrumentType"] == "share": 1844 view["stat"]["Shares"].append(statData) 1845 1846 elif item["instrumentType"] == "bond": 1847 view["stat"]["Bonds"].append(statData) 1848 1849 elif item["instrumentType"] == "etf": 1850 view["stat"]["Etfs"].append(statData) 1851 1852 elif item["instrumentType"] == "Futures": 1853 view["stat"]["Futures"].append(statData) 1854 1855 else: 1856 continue 1857 1858 # total changes in Russian Ruble: 1859 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1860 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1861 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1862 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1863 view["stat"]["funds"]["rub"] = { 1864 "total": view["stat"]["availableRUB"], 1865 "totalCostRUB": view["stat"]["availableRUB"], 1866 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1867 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1868 } 1869 1870 # --- pending orders sector data: 1871 uniquePendingOrders = [] 1872 uniquePendingOrdersFIGIs = [] 1873 for item in view["raw"]["orders"]: 1874 if item["figi"] not in uniquePendingOrdersFIGIs: 1875 uniquePendingOrdersFIGIs.append(item["figi"]) 1876 uniquePendingOrders.append(item) 1877 1878 for item in uniquePendingOrders: 1879 self.figi = item["figi"] 1880 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1881 1882 if instrument: 1883 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1884 orderType = TKS_ORDER_TYPES[item["orderType"]] 1885 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1886 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1887 1888 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1889 if item["direction"] == "ORDER_DIRECTION_BUY": 1890 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1891 1892 else: 1893 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1894 1895 # requested price for order execution: 1896 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1897 1898 # necessary changes in percent to reach target from current price: 1899 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1900 1901 view["stat"]["orders"].append({ 1902 "orderID": item["orderId"], # orderId number parameter of current order 1903 "figi": item["figi"], # FIGI identification 1904 "ticker": instrument["ticker"], # ticker name by FIGI 1905 "lotsRequested": item["lotsRequested"], # requested lots value 1906 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1907 "currentPrice": lastPrice, # current instrument's price for defined action 1908 "targetPrice": target, # requested price for order execution in base currency 1909 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1910 "percentChanges": changes, # changes in percent to target from current price 1911 "currency": item["currency"], # instrument's currency name 1912 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1913 "type": orderType, # type of order from TKS_ORDER_TYPES 1914 "status": orderState, # order status from TKS_ORDER_STATES 1915 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1916 }) 1917 1918 # --- stop orders sector data: 1919 uniqueStopOrders = [] 1920 uniqueStopOrdersFIGIs = [] 1921 for item in view["raw"]["stopOrders"]: 1922 if item["figi"] not in uniqueStopOrdersFIGIs: 1923 uniqueStopOrdersFIGIs.append(item["figi"]) 1924 uniqueStopOrders.append(item) 1925 1926 for item in uniqueStopOrders: 1927 self.figi = item["figi"] 1928 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1929 1930 if instrument: 1931 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1932 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1933 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1934 1935 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1936 if "expirationTime" in item.keys(): 1937 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1938 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1939 1940 else: 1941 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1942 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1943 1944 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1945 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1946 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1947 1948 else: 1949 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1950 1951 # requested price when stop-order executed: 1952 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1953 1954 # price for limit-order, set up when stop-order executed: 1955 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1956 1957 # necessary changes in percent to reach target from current price: 1958 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1959 1960 view["stat"]["stopOrders"].append({ 1961 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1962 "figi": item["figi"], # FIGI identification 1963 "ticker": instrument["ticker"], # ticker name by FIGI 1964 "lotsRequested": item["lotsRequested"], # requested lots value 1965 "currentPrice": lastPrice, # current instrument's price for defined action 1966 "targetPrice": target, # requested price for stop-order execution in base currency 1967 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1968 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1969 "percentChanges": changes, # changes in percent to target from current price 1970 "currency": item["currency"], # instrument's currency name 1971 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1972 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1973 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1974 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1975 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1976 }) 1977 1978 # --- calculating data for analytics section: 1979 # portfolio distribution by assets: 1980 view["analytics"]["distrByAssets"] = { 1981 "Ruble": { 1982 "uniques": 1, 1983 "cost": view["stat"]["availableRUB"], 1984 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1985 }, 1986 "Currencies": { 1987 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1988 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1989 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1990 }, 1991 "Shares": { 1992 "uniques": len(view["stat"]["Shares"]), 1993 "cost": view["stat"]["sharesCostRUB"], 1994 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1995 }, 1996 "Bonds": { 1997 "uniques": len(view["stat"]["Bonds"]), 1998 "cost": view["stat"]["bondsCostRUB"], 1999 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2000 }, 2001 "Etfs": { 2002 "uniques": len(view["stat"]["Etfs"]), 2003 "cost": view["stat"]["etfsCostRUB"], 2004 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2005 }, 2006 "Futures": { 2007 "uniques": len(view["stat"]["Futures"]), 2008 "cost": view["stat"]["futuresCostRUB"], 2009 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2010 }, 2011 } 2012 2013 # portfolio distribution by companies: 2014 view["analytics"]["distrByCompanies"]["All money cash"] = { 2015 "ticker": "", 2016 "cost": view["stat"]["allCurrenciesCostRUB"], 2017 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2018 } 2019 view["analytics"]["distrByCompanies"].update(byComp) 2020 2021 # portfolio distribution by sectors: 2022 view["analytics"]["distrBySectors"]["All money cash"] = { 2023 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2024 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2025 } 2026 view["analytics"]["distrBySectors"].update(bySect) 2027 2028 # portfolio distribution by currencies: 2029 view["analytics"]["distrByCurrencies"].update(byCurr) 2030 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2031 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2032 2033 # portfolio distribution by countries: 2034 view["analytics"]["distrByCountries"].update(byCountry) 2035 2036 # --- Prepare text statistics overview in human-readable: 2037 if show: 2038 # Whatever the value `details`, header not changes: 2039 info = [ 2040 "# Client's portfolio\n\n", 2041 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2042 "* **Account ID:** [{}]\n".format(self.accountId), 2043 ] 2044 2045 if details in ["full", "positions", "digest"]: 2046 info.extend([ 2047 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2048 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2049 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2050 view["stat"]["totalChangesRUB"], 2051 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2052 view["stat"]["totalChangesPercentRUB"], 2053 ), 2054 ]) 2055 2056 if details in ["full", "positions"]: 2057 info.extend([ 2058 "## Open positions\n\n", 2059 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2060 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2061 "| Ruble | {:>31} | | | | | |\n".format( 2062 "{:.2f} ({:.2f}) rub".format( 2063 view["stat"]["availableRUB"], 2064 view["stat"]["blockedRUB"], 2065 ) 2066 ) 2067 ]) 2068 2069 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2070 return [ 2071 "| | | | | | | |\n", 2072 "| {:<27} | | | | | {:>19} | |\n".format( 2073 noTradeStr if noTradeStr else typeStr, 2074 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2075 ), 2076 ] 2077 2078 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2079 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2080 "{} [{}]".format(data["ticker"], data["figi"]), 2081 "{:.2f} ({:.2f}) {}".format( 2082 data["volume"], 2083 data["blocked"], 2084 data["currency"], 2085 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2086 data["volume"], 2087 data["blocked"], 2088 ), 2089 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2090 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2091 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2092 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2093 "{}{:.2f} {} ({}{:.2f}%)".format( 2094 "+" if data["profit"] > 0 else "", 2095 data["profit"], data["baseCurrencyName"], 2096 "+" if data["percentProfit"] > 0 else "", 2097 data["percentProfit"], 2098 ), 2099 ) 2100 2101 # --- Show currencies section: 2102 if view["stat"]["Currencies"]: 2103 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2104 for item in view["stat"]["Currencies"]: 2105 info.append(_InfoStr(item, showCurrencyName=True)) 2106 2107 else: 2108 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2109 2110 # --- Show shares section: 2111 if view["stat"]["Shares"]: 2112 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2113 2114 for item in view["stat"]["Shares"]: 2115 info.append(_InfoStr(item)) 2116 2117 else: 2118 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2119 2120 # --- Show bonds section: 2121 if view["stat"]["Bonds"]: 2122 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2123 2124 for item in view["stat"]["Bonds"]: 2125 info.append(_InfoStr(item)) 2126 2127 else: 2128 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2129 2130 # --- Show etfs section: 2131 if view["stat"]["Etfs"]: 2132 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2133 2134 for item in view["stat"]["Etfs"]: 2135 info.append(_InfoStr(item)) 2136 2137 else: 2138 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2139 2140 # --- Show futures section: 2141 if view["stat"]["Futures"]: 2142 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2143 2144 for item in view["stat"]["Futures"]: 2145 info.append(_InfoStr(item)) 2146 2147 else: 2148 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2149 2150 if details in ["full", "orders"]: 2151 # --- Show pending orders section: 2152 if view["stat"]["orders"]: 2153 info.extend([ 2154 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2155 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2156 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2157 ]) 2158 2159 for item in view["stat"]["orders"]: 2160 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2161 "{} [{}]".format(item["ticker"], item["figi"]), 2162 item["orderID"], 2163 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2164 "{} {} ({}{:.2f}%)".format( 2165 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2166 item["baseCurrencyName"], 2167 "+" if item["percentChanges"] > 0 else "", 2168 float(item["percentChanges"]), 2169 ), 2170 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2171 item["action"], 2172 item["type"], 2173 item["date"], 2174 )) 2175 2176 else: 2177 info.append("\n## Total pending limit-orders: 0\n") 2178 2179 # --- Show stop orders section: 2180 if view["stat"]["stopOrders"]: 2181 info.extend([ 2182 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2183 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2184 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2185 ]) 2186 2187 for item in view["stat"]["stopOrders"]: 2188 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2189 "{} [{}]".format(item["ticker"], item["figi"]), 2190 item["orderID"], 2191 item["lotsRequested"], 2192 "{} {} ({}{:.2f}%)".format( 2193 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2194 item["baseCurrencyName"], 2195 "+" if item["percentChanges"] > 0 else "", 2196 float(item["percentChanges"]), 2197 ), 2198 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2199 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2200 item["action"], 2201 item["type"], 2202 item["expType"], 2203 item["createDate"], 2204 item["expDate"], 2205 )) 2206 2207 else: 2208 info.append("\n## Total stop-orders: 0\n") 2209 2210 if details in ["full", "analytics"]: 2211 # -- Show analytics section: 2212 if view["stat"]["portfolioCostRUB"] > 0: 2213 info.extend([ 2214 "\n# Analytics\n" 2215 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2216 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2217 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2218 view["stat"]["totalChangesRUB"], 2219 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2220 view["stat"]["totalChangesPercentRUB"], 2221 ), 2222 "\n## Portfolio distribution by assets\n" 2223 "\n| Type | Uniques | Percent | Current cost |\n", 2224 "|------------|---------|---------|--------------------|\n", 2225 ]) 2226 2227 for key in view["analytics"]["distrByAssets"].keys(): 2228 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2229 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2230 key, 2231 view["analytics"]["distrByAssets"][key]["uniques"], 2232 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2233 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2234 )) 2235 2236 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2237 info.extend([ 2238 "\n## Portfolio distribution by companies\n" 2239 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2240 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2241 ]) 2242 2243 for company in view["analytics"]["distrByCompanies"].keys(): 2244 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2245 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2246 info.append("| {} | {:<7} | {:<18} |\n".format( 2247 "{}{}{}".format( 2248 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2249 company, 2250 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2251 ), 2252 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2253 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2254 )) 2255 2256 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2257 info.extend([ 2258 "\n## Portfolio distribution by sectors\n" 2259 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2260 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2261 ]) 2262 2263 for sector in view["analytics"]["distrBySectors"].keys(): 2264 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2265 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2266 sector, 2267 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2268 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2269 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2270 )) 2271 2272 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2273 info.extend([ 2274 "\n## Portfolio distribution by currencies\n" 2275 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2276 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2277 ]) 2278 2279 for curr in view["analytics"]["distrByCurrencies"].keys(): 2280 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2281 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2282 info.append("| {} | {:<7} | {:<18} |\n".format( 2283 "[{}] {}{}".format( 2284 curr, 2285 view["analytics"]["distrByCurrencies"][curr]["name"], 2286 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2287 ), 2288 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2289 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2290 )) 2291 2292 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2293 info.extend([ 2294 "\n## Portfolio distribution by countries\n" 2295 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2296 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2297 ]) 2298 2299 for country in view["analytics"]["distrByCountries"].keys(): 2300 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2301 nameLen = len(country) 2302 info.append("| {} | {:<7} | {:<18} |\n".format( 2303 "{}{}".format( 2304 country, 2305 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2306 ), 2307 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2308 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2309 )) 2310 2311 infoText = "".join(info) 2312 2313 uLogger.info(infoText) 2314 2315 if details == "full" and self.overviewFile: 2316 filename = self.overviewFile 2317 2318 elif details == "digest" and self.overviewDigestFile: 2319 filename = self.overviewDigestFile 2320 2321 elif details == "positions" and self.overviewPositionsFile: 2322 filename = self.overviewPositionsFile 2323 2324 elif details == "orders" and self.overviewOrdersFile: 2325 filename = self.overviewOrdersFile 2326 2327 elif details == "analytics" and self.overviewAnalyticsFile: 2328 filename = self.overviewAnalyticsFile 2329 2330 else: 2331 filename = "" 2332 2333 if filename: 2334 with open(filename, "w", encoding="UTF-8") as fH: 2335 fH.write(infoText) 2336 2337 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2338 2339 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be? You should specify one of strings:
full- shows full available information about portfolio status (by default),positions- shows only open positions,digest- show a short digest of the portfolio status,analytics- shows only the analytics section and the distribution of the portfolio by various categories,orders- shows only sections of open limits and stop orders.
Returns
dictionary with client's raw portfolio and some statistics.
2341 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2342 """ 2343 Returns history operations between two given dates for current `accountId`. 2344 If `reportFile` string is not empty then also save human-readable report. 2345 Shows some statistical data of closed positions. 2346 2347 :param start: see docstring in `GetDatesAsString()` method 2348 :param end: see docstring in `GetDatesAsString()` method 2349 :param show: if `True` then also prints all records to the console. 2350 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2351 :return: original list of dictionaries with history of deals records from API ("operations" key): 2352 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2353 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2354 """ 2355 if self.accountId is None or not self.accountId: 2356 uLogger.error("Variable `accountId` must be defined for using this method!") 2357 raise Exception("Account ID required") 2358 2359 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2360 2361 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2362 2363 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2364 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2365 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2366 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2367 customStat = {} # custom statistics in additional to responseJSON 2368 2369 # --- output report in human-readable format: 2370 if show or self.reportFile: 2371 splitLine1 = "| | | | | |\n" # Summary section 2372 splitLine2 = "| | | | | | | | |\n" # Operations section 2373 nextDay = "" 2374 2375 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2376 2377 if len(ops) > 0: 2378 customStat = { 2379 "opsCount": 0, # total operations count 2380 "buyCount": 0, # buy operations 2381 "sellCount": 0, # sell operations 2382 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2383 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2384 "payIn": {"rub": 0.}, # Deposit brokerage account 2385 "payOut": {"rub": 0.}, # Withdrawals 2386 "divs": {"rub": 0.}, # Dividends income 2387 "coupons": {"rub": 0.}, # Coupon's income 2388 "brokerCom": {"rub": 0.}, # Service commissions 2389 "serviceCom": {"rub": 0.}, # Service commissions 2390 "marginCom": {"rub": 0.}, # Margin commissions 2391 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2392 } 2393 2394 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2395 for item in ops: 2396 if item["state"] == "OPERATION_STATE_EXECUTED": 2397 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2398 2399 # count buy operations: 2400 if "_BUY" in item["operationType"]: 2401 customStat["buyCount"] += 1 2402 2403 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2404 customStat["buyTotal"][item["payment"]["currency"]] += payment 2405 2406 else: 2407 customStat["buyTotal"][item["payment"]["currency"]] = payment 2408 2409 # count sell operations: 2410 elif "_SELL" in item["operationType"]: 2411 customStat["sellCount"] += 1 2412 2413 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2414 customStat["sellTotal"][item["payment"]["currency"]] += payment 2415 2416 else: 2417 customStat["sellTotal"][item["payment"]["currency"]] = payment 2418 2419 # count incoming operations: 2420 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2421 if item["payment"]["currency"] in customStat["payIn"].keys(): 2422 customStat["payIn"][item["payment"]["currency"]] += payment 2423 2424 else: 2425 customStat["payIn"][item["payment"]["currency"]] = payment 2426 2427 # count withdrawals operations: 2428 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2429 if item["payment"]["currency"] in customStat["payOut"].keys(): 2430 customStat["payOut"][item["payment"]["currency"]] += payment 2431 2432 else: 2433 customStat["payOut"][item["payment"]["currency"]] = payment 2434 2435 # count dividends income: 2436 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2437 if item["payment"]["currency"] in customStat["divs"].keys(): 2438 customStat["divs"][item["payment"]["currency"]] += payment 2439 2440 else: 2441 customStat["divs"][item["payment"]["currency"]] = payment 2442 2443 # count coupon's income: 2444 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2445 if item["payment"]["currency"] in customStat["coupons"].keys(): 2446 customStat["coupons"][item["payment"]["currency"]] += payment 2447 2448 else: 2449 customStat["coupons"][item["payment"]["currency"]] = payment 2450 2451 # count broker commissions: 2452 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2453 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2454 customStat["brokerCom"][item["payment"]["currency"]] += payment 2455 2456 else: 2457 customStat["brokerCom"][item["payment"]["currency"]] = payment 2458 2459 # count service commissions: 2460 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2461 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2462 customStat["serviceCom"][item["payment"]["currency"]] += payment 2463 2464 else: 2465 customStat["serviceCom"][item["payment"]["currency"]] = payment 2466 2467 # count margin commissions: 2468 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2469 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2470 customStat["marginCom"][item["payment"]["currency"]] += payment 2471 2472 else: 2473 customStat["marginCom"][item["payment"]["currency"]] = payment 2474 2475 # count withholding taxes: 2476 elif "_TAX" in item["operationType"]: 2477 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2478 customStat["allTaxes"][item["payment"]["currency"]] += payment 2479 2480 else: 2481 customStat["allTaxes"][item["payment"]["currency"]] = payment 2482 2483 else: 2484 continue 2485 2486 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2487 2488 # --- view "Actions" lines: 2489 info.extend([ 2490 "| 1 | 2 | 3 | 4 | 5 |\n", 2491 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2492 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2493 "| | Buy: {:<22} | {:<28} | | |\n".format( 2494 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2495 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2496 ), 2497 "| | Sell: {:<21} | {:<28} | | |\n".format( 2498 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2499 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2500 ), 2501 ]) 2502 2503 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2504 for key in opsKeys: 2505 if key == "rub": 2506 continue 2507 2508 info.extend([ 2509 "| | | {:<28} | | |\n".format( 2510 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2511 ), 2512 "| | | {:<28} | | |\n".format( 2513 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2514 ), 2515 ]) 2516 2517 info.append(splitLine1) 2518 2519 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2520 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2521 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2522 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2523 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2524 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2525 ) 2526 2527 # --- view "Payments" lines: 2528 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2529 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2530 2531 for key in paymentsKeys: 2532 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2533 2534 info.append(splitLine1) 2535 2536 # --- view "Commissions and taxes" lines: 2537 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2538 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2539 2540 for key in comKeys: 2541 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2542 2543 info.append(splitLine1) 2544 2545 info.extend([ 2546 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2547 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2548 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2549 ]) 2550 2551 else: 2552 info.append("Broker returned no operations during this period\n") 2553 2554 # --- view "Operations" section: 2555 for item in ops: 2556 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2557 continue 2558 2559 else: 2560 self.figi = item["figi"] if item["figi"] else "" 2561 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2562 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2563 2564 # group of deals during one day: 2565 if nextDay and item["date"].split("T")[0] != nextDay: 2566 info.append(splitLine2) 2567 nextDay = "" 2568 2569 else: 2570 nextDay = item["date"].split("T")[0] # saving current day for splitting 2571 2572 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2573 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2574 self.figi if self.figi else "—", 2575 instrument["ticker"] if instrument else "—", 2576 instrument["type"] if instrument else "—", 2577 item["quantity"] if int(item["quantity"]) > 0 else "—", 2578 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2579 TKS_OPERATION_STATES[item["state"]], 2580 TKS_OPERATION_TYPES[item["operationType"]], 2581 )) 2582 2583 infoText = "".join(info) 2584 2585 if show: 2586 uLogger.info(infoText) 2587 2588 if self.reportFile: 2589 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2590 fH.write(infoText) 2591 2592 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2593 2594 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
GetDatesAsString()method - end: see docstring in
GetDatesAsString()method - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2596 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2597 """ 2598 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2599 2600 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2601 Warning! Broker server used ISO UTC time by default. 2602 2603 If `historyFile` is not `None` then method save history to file, otherwise return only pandas dataframe. 2604 Also, `historyFile` used to update history with `onlyMissing` parameter. 2605 2606 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2607 2608 :param start: see docstring in `GetDatesAsString()` method. 2609 :param end: see docstring in `GetDatesAsString()` method. 2610 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2611 `"hour"`, `"day"`. Default: `"hour"`. 2612 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2613 False by default. Warning! History appends only from last candle to current time 2614 with always update last candle! 2615 :param csvSep: separator if csv-file is used, `,` by default. 2616 :param show: if `True` then also prints pandas dataframe to the console. 2617 :return: pandas dataframe with prices history. Headers of columns are defined by default: 2618 `["date", "time", "open", "high", "low", "close", "volume"]`. 2619 """ 2620 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2621 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2622 history = None # empty pandas object for history 2623 2624 if interval not in TKS_CANDLE_INTERVALS.keys(): 2625 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2626 raise Exception("Incorrect value") 2627 2628 if not (self.ticker or self.figi): 2629 uLogger.error("Ticker or FIGI must be defined!") 2630 raise Exception("Ticker or FIGI required") 2631 2632 if self.ticker and not self.figi: 2633 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2634 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2635 2636 if self.figi and not self.ticker: 2637 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2638 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2639 2640 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2641 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2642 if interval.lower() != "day": 2643 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2644 2645 delta = dtEnd - dtStart # current UTC time minus last time in file 2646 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2647 2648 # calculate history length in candles: 2649 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2650 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2651 length += 1 # to avoid fraction time 2652 2653 # calculate data blocks count: 2654 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2655 2656 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2657 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2658 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2659 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2660 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2661 2662 tempOld = None # pandas object for old history, if --only-missing key present 2663 lastTime = None # datetime object of last old candle in file 2664 2665 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2666 uLogger.debug("--only-missing key present, add only last missing candles...") 2667 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2668 2669 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2670 2671 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2672 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2673 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2674 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2675 2676 # get last datetime object from last string in file or minus 1 delta if file is empty: 2677 if len(tempOld) > 0: 2678 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2679 2680 else: 2681 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2682 2683 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2684 2685 responseJSONs = [] # raw history blocks of data 2686 2687 blockEnd = dtEnd 2688 for item in range(blocks): 2689 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2690 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2691 2692 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2693 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2694 )) 2695 2696 if blockStart == blockEnd: 2697 uLogger.debug("Skipped this zero-length block...") 2698 2699 else: 2700 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2701 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2702 self.body = str({ 2703 "figi": self.figi, 2704 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2705 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2706 "interval": TKS_CANDLE_INTERVALS[interval][0] 2707 }) 2708 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2709 2710 if "code" in responseJSON.keys(): 2711 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2712 2713 else: 2714 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2715 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2716 2717 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2718 2719 blockEnd = blockStart 2720 2721 printCount = len(responseJSONs) # candles to show in console 2722 if responseJSONs: 2723 tempHistory = pd.DataFrame( 2724 data={ 2725 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2726 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2727 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2728 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2729 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2730 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2731 "volume": [int(item["volume"]) for item in responseJSONs], 2732 }, 2733 index=range(len(responseJSONs)), 2734 columns=["date", "time", "open", "high", "low", "close", "volume"], 2735 ) 2736 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2737 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2738 2739 # append only newest candles to old history if --only-missing key present: 2740 if onlyMissing and tempOld is not None and lastTime is not None: 2741 index = 0 # find start index in tempHistory data: 2742 2743 for i, item in tempHistory.iterrows(): 2744 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2745 2746 if curTime == lastTime: 2747 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2748 index = i 2749 printCount = index + 1 2750 break 2751 2752 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2753 2754 else: 2755 history = tempHistory # if no `--only-missing` key then load full data from server 2756 2757 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2758 2759 if history is not None and not history.empty: 2760 if show: 2761 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2762 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2763 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2764 )) 2765 2766 else: 2767 uLogger.warning("Received an empty candles history!") 2768 2769 if self.historyFile is not None: 2770 if history is not None and not history.empty: 2771 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2772 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2773 2774 else: 2775 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2776 2777 else: 2778 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only pandas dataframe returns.") 2779 2780 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only pandas dataframe.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
GetDatesAsString()method. - end: see docstring in
GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints pandas dataframe to the console.
Returns
pandas dataframe with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2782 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2783 """ 2784 Load candles history from csv-file and return pandas dataframe object. 2785 2786 See also: `History()` and `ShowHistoryChart()` methods. 2787 2788 :param filePath: path to csv-file to open. 2789 """ 2790 loadedHistory = None # init candles data object 2791 2792 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2793 2794 if os.path.exists(filePath): 2795 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as pandas dataframe 2796 2797 tfStr = self.priceModel.FormattedDelta( 2798 self.priceModel.timeframe, 2799 "{days} days {hours}h {minutes}m {seconds}s", 2800 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2801 self.priceModel.timeframe, 2802 "{hours}h {minutes}m {seconds}s", 2803 ) 2804 2805 if loadedHistory is not None and not loadedHistory.empty: 2806 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2807 len(loadedHistory), 2808 tfStr, 2809 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2810 ) 2811 2812 else: 2813 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2814 2815 else: 2816 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2817 2818 return loadedHistory
Load candles history from csv-file and return pandas dataframe object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2820 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2821 """ 2822 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2823 2824 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2825 Default: `index.html` (both for interact and non-interact candlesticks chart). 2826 2827 See also: `History()` and `LoadHistory()` methods. 2828 2829 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2830 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2831 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2832 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2833 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2834 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2835 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2836 """ 2837 if isinstance(candles, str): 2838 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2839 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2840 2841 elif isinstance(candles, pd.DataFrame): 2842 self.priceModel.prices = candles # set candles chain from variable 2843 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2844 2845 if "datetime" not in candles.columns: 2846 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2847 2848 else: 2849 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2850 raise Exception("Incorrect value") 2851 2852 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2853 2854 if interact: 2855 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2856 2857 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2858 2859 else: 2860 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2861 2862 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2863 2864 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2866 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2867 """ 2868 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2869 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2870 2871 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2872 2873 :param operation: string "Buy" or "Sell". 2874 :param lots: volume, integer count of lots >= 1. 2875 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2876 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2877 :param expDate: string "Undefined" by default or local date in future, 2878 it is a string with format `%Y-%m-%d %H:%M:%S`. 2879 :return: JSON with response from broker server. 2880 """ 2881 if self.accountId is None or not self.accountId: 2882 uLogger.error("Variable `accountId` must be defined for using this method!") 2883 raise Exception("Account ID required") 2884 2885 if operation is None or not operation or operation not in ("Buy", "Sell"): 2886 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2887 raise Exception("Incorrect value") 2888 2889 if lots is None or lots < 1: 2890 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2891 lots = 1 2892 2893 if tp is None or tp < 0: 2894 tp = 0 2895 2896 if sl is None or sl < 0: 2897 sl = 0 2898 2899 if expDate is None or not expDate: 2900 expDate = "Undefined" 2901 2902 if not (self.ticker or self.figi): 2903 uLogger.error("Ticker or FIGI must be defined!") 2904 raise Exception("Ticker or FIGI required") 2905 2906 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2907 self.ticker = instrument["ticker"] 2908 self.figi = instrument["figi"] 2909 2910 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2911 2912 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2913 self.body = str({ 2914 "figi": self.figi, 2915 "quantity": str(lots), 2916 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2917 "accountId": str(self.accountId), 2918 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2919 }) 2920 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2921 2922 if "orderId" in response.keys(): 2923 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2924 operation, response["orderId"], 2925 self.ticker, self.figi, lots, 2926 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2927 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2928 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2929 )) 2930 2931 else: 2932 uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.") 2933 2934 if tp > 0: 2935 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2936 2937 if sl > 0: 2938 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2939 2940 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2942 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2943 """ 2944 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2945 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2946 2947 See also: `Order()` and `Trade()` docstrings. 2948 2949 :param lots: volume, integer count of lots >= 1. 2950 :param tp: float > 0, take profit price of stop-order. 2951 :param sl: float > 0, stop loss price of stop-order. 2952 :param expDate: it's a local date in future. 2953 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2954 :return: JSON with response from broker server. 2955 """ 2956 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2958 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2959 """ 2960 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2961 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2962 2963 See also: `Order()` and `Trade()` docstrings. 2964 2965 :param lots: volume, integer count of lots >= 1. 2966 :param tp: float > 0, take profit price of stop-order. 2967 :param sl: float > 0, stop loss price of stop-order. 2968 :param expDate: it's a local date in the future. 2969 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2970 :return: JSON with response from broker server. 2971 """ 2972 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2974 def CloseTrades(self, tickers: list, portfolio: dict = None) -> None: 2975 """ 2976 Close position of given instruments. 2977 2978 :param tickers: tickers list of instruments that must be closed. 2979 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2980 This avoids unnecessary downloading data from the server. 2981 """ 2982 if not tickers: 2983 uLogger.info("Tickers list is empty, nothing to close.") 2984 2985 else: 2986 if portfolio is None or not portfolio: 2987 portfolio = self.Overview(show=False) 2988 2989 allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2990 uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers)) 2991 2992 for ticker in tickers: 2993 if ticker not in allOpenedTickers: 2994 uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker)) 2995 continue 2996 2997 # search open trade info about instrument by ticker: 2998 instrument = {} 2999 for iType in TKS_INSTRUMENTS: 3000 if instrument: 3001 break 3002 3003 for item in portfolio["stat"][iType]: 3004 if item["ticker"] == ticker: 3005 instrument = item 3006 break 3007 3008 if instrument: 3009 self.ticker = ticker 3010 self.figi = instrument["figi"] 3011 3012 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3013 self.ticker, 3014 self.figi, 3015 int(instrument["volume"]), 3016 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3017 )) 3018 3019 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3020 3021 if tradeLots > 0: 3022 if instrument["blocked"] > 0: 3023 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3024 instrument["blocked"], 3025 self.ticker, 3026 tradeLots, 3027 )) 3028 3029 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3030 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3031 3032 else: 3033 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
Close position of given instruments.
Parameters
- tickers: tickers list of instruments that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3035 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3036 """ 3037 Close all positions of given instruments with defined type. 3038 3039 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3040 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3041 This avoids unnecessary downloading data from the server. 3042 """ 3043 if iType not in TKS_INSTRUMENTS: 3044 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3045 3046 else: 3047 if portfolio is None or not portfolio: 3048 portfolio = self.Overview(show=False) 3049 3050 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3051 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3052 3053 if tickers and portfolio: 3054 self.CloseTrades(tickers, portfolio) 3055 3056 else: 3057 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3059 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3060 """ 3061 Universal method to create market or limit orders with all available parameters for current `accountId`. 3062 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3063 3064 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3065 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3066 3067 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3068 then broker immediately open market order as you can do simple --buy or --sell operations! 3069 3070 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3071 When current price will go up or down to target price value then broker opens a limit order. 3072 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3073 3074 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3075 3076 :param operation: string "Buy" or "Sell". 3077 :param orderType: string "Limit" or "Stop". 3078 :param lots: volume, integer count of lots >= 1. 3079 :param targetPrice: target price > 0. This is open trade price for limit order. 3080 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3081 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3082 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3083 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3084 Stop loss order always executed by market price. 3085 :param expDate: string "Undefined" by default or local date in future. 3086 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3087 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3088 A limit order has no expiration date, it lasts until the end of the trading day. 3089 :return: JSON with response from broker server. 3090 """ 3091 if self.accountId is None or not self.accountId: 3092 uLogger.error("Variable `accountId` must be defined for using this method!") 3093 raise Exception("Account ID required") 3094 3095 if operation is None or not operation or operation not in ("Buy", "Sell"): 3096 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3097 raise Exception("Incorrect value") 3098 3099 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3100 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3101 raise Exception("Incorrect value") 3102 3103 if lots is None or lots < 1: 3104 uLogger.error("You must define trade volume > 0: integer count of lots!") 3105 raise Exception("Incorrect value") 3106 3107 if targetPrice is None or targetPrice <= 0: 3108 uLogger.error("Target price for limit-order must be greater than 0!") 3109 raise Exception("Incorrect value") 3110 3111 if limitPrice is None or limitPrice <= 0: 3112 limitPrice = targetPrice 3113 3114 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3115 stopType = "Limit" 3116 3117 if expDate is None or not expDate: 3118 expDate = "Undefined" 3119 3120 if not (self.ticker or self.figi): 3121 uLogger.error("Tocker or FIGI must be defined!") 3122 raise Exception("Ticker or FIGI required") 3123 3124 response = {} 3125 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3126 self.ticker = instrument["ticker"] 3127 self.figi = instrument["figi"] 3128 3129 if orderType == "Limit": 3130 uLogger.debug( 3131 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3132 self.ticker, self.figi, 3133 operation, lots, targetPrice, instrument["currency"], 3134 )) 3135 3136 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3137 self.body = str({ 3138 "figi": self.figi, 3139 "quantity": str(lots), 3140 "price": FloatToNano(targetPrice), 3141 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3142 "accountId": str(self.accountId), 3143 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3144 }) 3145 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3146 3147 if "orderId" in response.keys(): 3148 uLogger.info( 3149 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3150 response["orderId"], 3151 self.ticker, self.figi, 3152 operation, lots, targetPrice, instrument["currency"], 3153 )) 3154 3155 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3156 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3157 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3158 targetPrice, instrument["currency"], 3159 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3160 )) 3161 3162 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3163 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3164 targetPrice, instrument["currency"], 3165 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3166 )) 3167 3168 else: 3169 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3170 3171 if orderType == "Stop": 3172 uLogger.debug( 3173 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3174 self.ticker, self.figi, 3175 operation, lots, 3176 targetPrice, instrument["currency"], 3177 limitPrice, instrument["currency"], 3178 stopType, expDate, 3179 )) 3180 3181 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3182 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3183 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3184 3185 body = { 3186 "figi": self.figi, 3187 "quantity": str(lots), 3188 "price": FloatToNano(limitPrice), 3189 "stopPrice": FloatToNano(targetPrice), 3190 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3191 "accountId": str(self.accountId), 3192 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3193 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3194 } 3195 3196 if expDateUTC: 3197 body["expireDate"] = expDateUTC 3198 3199 self.body = str(body) 3200 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3201 3202 if "stopOrderId" in response.keys(): 3203 uLogger.info( 3204 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3205 response["stopOrderId"], 3206 self.ticker, self.figi, 3207 operation, lots, 3208 targetPrice, instrument["currency"], 3209 limitPrice, instrument["currency"], 3210 TKS_STOP_ORDER_TYPES[stopOrderType], 3211 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3212 )) 3213 3214 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3215 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3216 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3217 targetPrice, instrument["currency"], 3218 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3219 )) 3220 3221 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3222 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3223 targetPrice, instrument["currency"], 3224 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3225 )) 3226 3227 else: 3228 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3229 3230 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3232 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3233 """ 3234 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3235 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3236 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3237 See also: `Order()` docstring. 3238 3239 :param lots: volume, integer count of lots >= 1. 3240 :param targetPrice: target price > 0. This is open trade price for limit order. 3241 :return: JSON with response from broker server. 3242 """ 3243 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3245 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3246 """ 3247 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3248 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3249 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3250 target price value then broker opens a limit order. See also: `Order()` docstring. 3251 3252 :param lots: volume, integer count of lots >= 1. 3253 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3254 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3255 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3256 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3257 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3258 :param expDate: string "Undefined" by default or local date in future. 3259 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3260 This date is converting to UTC format for server. 3261 :return: JSON with response from broker server. 3262 """ 3263 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3265 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3266 """ 3267 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3268 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3269 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3270 See also: `Order()` docstring. 3271 3272 :param lots: volume, integer count of lots >= 1. 3273 :param targetPrice: target price > 0. This is open trade price for limit order. 3274 :return: JSON with response from broker server. 3275 """ 3276 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3278 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3279 """ 3280 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3281 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3282 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3283 target price value then broker opens a limit order. See also: `Order()` docstring. 3284 3285 :param lots: volume, integer count of lots >= 1. 3286 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3287 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3288 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3289 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3290 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3291 :param expDate: string "Undefined" by default or local date in future. 3292 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3293 This date is converting to UTC format for server. 3294 :return: JSON with response from broker server. 3295 """ 3296 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3298 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3299 """ 3300 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3301 3302 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3303 :param allOrdersIDs: pre-received lists of all active pending orders. 3304 This avoids unnecessary downloading data from the server. 3305 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3306 """ 3307 if self.accountId is None or not self.accountId: 3308 uLogger.error("Variable `accountId` must be defined for using this method!") 3309 raise Exception("Account ID required") 3310 3311 if orderIDs: 3312 if allOrdersIDs is None or not allOrdersIDs: 3313 rawOrders = self.RequestPendingOrders() 3314 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3315 3316 if allStopOrdersIDs is None or not allStopOrdersIDs: 3317 rawStopOrders = self.RequestStopOrders() 3318 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3319 3320 for orderID in orderIDs: 3321 idInPendingOrders = orderID in allOrdersIDs 3322 idInStopOrders = orderID in allStopOrdersIDs 3323 3324 if not (idInPendingOrders or idInStopOrders): 3325 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3326 continue 3327 3328 else: 3329 if idInPendingOrders: 3330 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3331 3332 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3333 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3334 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3335 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3336 3337 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3338 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3339 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3340 3341 else: 3342 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3343 3344 elif idInStopOrders: 3345 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3346 3347 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3348 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3349 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3350 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3351 3352 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3353 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3354 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3355 3356 else: 3357 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3358 3359 else: 3360 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3362 def CloseAllOrders(self) -> None: 3363 """ 3364 Gets a list of open pending and stop orders and cancel it all. 3365 """ 3366 rawOrders = self.RequestPendingOrders() 3367 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3368 lenOrders = len(allOrdersIDs) 3369 3370 rawStopOrders = self.RequestStopOrders() 3371 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3372 lenSOrders = len(allStopOrdersIDs) 3373 3374 if lenOrders > 0 or lenSOrders > 0: 3375 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3376 3377 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3378 3379 else: 3380 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3382 def CloseAll(self, *args) -> None: 3383 """ 3384 Close all available (not blocked) opened trades and orders. 3385 3386 Also, you can select one or more keywords case-insensitive: 3387 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3388 3389 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3390 """ 3391 overview = self.Overview(show=False) # get all open trades info 3392 3393 if len(args) == 0: 3394 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3395 self.CloseAllOrders() # close all pending and stop orders 3396 3397 for iType in TKS_INSTRUMENTS: 3398 if iType != "Currencies": 3399 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3400 3401 else: 3402 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3403 lowerArgs = [x.lower() for x in args] 3404 3405 if "orders" in lowerArgs: 3406 self.CloseAllOrders() # close all pending and stop orders 3407 3408 for iType in TKS_INSTRUMENTS: 3409 if iType.lower() in lowerArgs and iType != "Currencies": 3410 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3412 @staticmethod 3413 def ParseOrderParameters(operation, **inputParameters): 3414 """ 3415 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3416 3417 :param operation: string "Buy" or "Sell". 3418 :param inputParameters: this is dict of strings that looks like this 3419 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3420 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3421 "prices" key: one or more prices to open limit-orders 3422 Counts of values in lots and prices lists must be equals! 3423 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3424 """ 3425 # TODO: update order grid work with api v2 3426 pass 3427 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3428 # 3429 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3430 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3431 # raise Exception("Incorrect value") 3432 # 3433 # if "l" in inputParameters.keys(): 3434 # inputParameters["lots"] = inputParameters.pop("l") 3435 # 3436 # if "p" in inputParameters.keys(): 3437 # inputParameters["prices"] = inputParameters.pop("p") 3438 # 3439 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3440 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3441 # raise Exception("Incorrect value") 3442 # 3443 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3444 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3445 # 3446 # if len(lots) != len(prices): 3447 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3448 # raise Exception("Incorrect value") 3449 # 3450 # uLogger.debug("Extracted parameters for orders:") 3451 # uLogger.debug("lots = {}".format(lots)) 3452 # uLogger.debug("prices = {}".format(prices)) 3453 # 3454 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3455 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3456 # uLogger.debug("Order parameters: {}".format(result)) 3457 # 3458 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3460 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3461 """ 3462 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3463 3464 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3465 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3466 """ 3467 result = False 3468 msg = "Instrument not defined!" 3469 3470 if portfolio is None or not portfolio: 3471 portfolio = self.Overview(show=False) 3472 3473 if self.ticker: 3474 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3475 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3476 3477 for iType in TKS_INSTRUMENTS: 3478 for instrument in portfolio["stat"][iType]: 3479 if instrument["ticker"] == self.ticker: 3480 result = True 3481 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3482 break 3483 3484 elif self.figi: 3485 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3486 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3487 3488 for iType in TKS_INSTRUMENTS: 3489 for instrument in portfolio["stat"][iType]: 3490 if instrument["figi"] == self.figi: 3491 result = True 3492 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3493 break 3494 3495 else: 3496 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3497 3498 uLogger.debug(msg) 3499 3500 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3502 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3503 """ 3504 Returns instrument is in the user's portfolio if it presents there. 3505 Instrument must be defined by `ticker` (highly priority) or `figi`. 3506 3507 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3508 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3509 """ 3510 result = None 3511 msg = "Instrument not defined!" 3512 3513 if portfolio is None or not portfolio: 3514 portfolio = self.Overview(show=False) 3515 3516 if self.ticker: 3517 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3518 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3519 3520 for iType in TKS_INSTRUMENTS: 3521 for instrument in portfolio["stat"][iType]: 3522 if instrument["ticker"] == self.ticker: 3523 result = instrument 3524 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3525 break 3526 3527 elif self.figi: 3528 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3529 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3530 3531 for iType in TKS_INSTRUMENTS: 3532 for instrument in portfolio["stat"][iType]: 3533 if instrument["figi"] == self.figi: 3534 result = instrument 3535 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3536 break 3537 3538 else: 3539 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3540 3541 uLogger.debug(msg) 3542 3543 return result
Returns instrument is in the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3545 def RequestLimits(self) -> dict: 3546 """ 3547 Method for obtaining the available funds for withdrawal for current `accountId`. 3548 3549 See also: 3550 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3551 - `OverviewLimits()` method 3552 3553 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3554 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3555 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3556 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3557 """ 3558 if self.accountId is None or not self.accountId: 3559 uLogger.error("Variable `accountId` must be defined for using this method!") 3560 raise Exception("Account ID required") 3561 3562 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3563 3564 self.body = str({"accountId": self.accountId}) 3565 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3566 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3567 3568 uLogger.debug("Records about available funds for withdrawal successfully received") 3569 3570 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3572 def OverviewLimits(self, show: bool = False) -> dict: 3573 """ 3574 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3575 3576 See also: `RequestLimits()`. 3577 3578 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3579 :return: dict with raw parsed data from server and some calculated statistics about it. 3580 """ 3581 if self.accountId is None or not self.accountId: 3582 uLogger.error("Variable `accountId` must be defined for using this method!") 3583 raise Exception("Account ID required") 3584 3585 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3586 3587 view = { 3588 "rawLimits": rawLimits, 3589 "limits": { # parsed data for every currency: 3590 "money": { # this is an array of portfolio currency positions 3591 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3592 }, 3593 "blocked": { # this is an array of blocked currency 3594 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3595 }, 3596 "blockedGuarantee": { # this is locked money under collateral for futures 3597 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3598 }, 3599 }, 3600 } 3601 3602 # --- Prepare text table with limits in human-readable format: 3603 if show: 3604 info = [ 3605 "# Withdrawal limits\n\n", 3606 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3607 "* **Account ID:** [{}]\n".format(self.accountId), 3608 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3609 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3610 ] 3611 3612 for curr in view["limits"]["money"].keys(): 3613 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3614 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3615 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3616 3617 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3618 "[{}]".format(curr), 3619 "{:.2f}".format(view["limits"]["money"][curr]), 3620 "{:.2f}".format(availableMoney), 3621 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3622 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3623 ) 3624 3625 if curr == "rub": 3626 info.insert(5, infoStr) # insert at first position in table and after headers 3627 3628 else: 3629 info.append(infoStr) 3630 3631 infoText = "".join(info) 3632 3633 uLogger.info(infoText) 3634 3635 if self.withdrawalLimitsFile: 3636 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3637 fH.write(infoText) 3638 3639 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3640 3641 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3643 def RequestAccounts(self) -> dict: 3644 """ 3645 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3646 3647 See also: 3648 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3649 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3650 - `OverviewUserInfo()` method 3651 3652 :return: dict with raw data from server that contains accounts info. Example of dict: 3653 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3654 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3655 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3656 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3657 """ 3658 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3659 3660 self.body = str({}) 3661 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3662 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3663 3664 uLogger.debug("Records about available accounts successfully received") 3665 3666 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
3668 def RequestUserInfo(self) -> dict: 3669 """ 3670 Method for requesting common user's information. 3671 3672 See also: 3673 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3674 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3675 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3676 - `OverviewUserInfo()` method 3677 3678 :return: dict with raw data from server that contains user's information. Example of dict: 3679 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3680 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3681 """ 3682 uLogger.debug("Requesting common user's information. Wait, please...") 3683 3684 self.body = str({}) 3685 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3686 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3687 3688 uLogger.debug("Records about current user successfully received") 3689 3690 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
3692 def RequestMarginStatus(self, accountId: str = None) -> dict: 3693 """ 3694 Method for requesting margin calculation for defined account ID. 3695 3696 See also: 3697 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3698 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3699 - `OverviewUserInfo()` method 3700 3701 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3702 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3703 Example of responses: 3704 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3705 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3706 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3707 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3708 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3709 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3710 """ 3711 if accountId is None or not accountId: 3712 if self.accountId is None or not self.accountId: 3713 uLogger.error("Variable `accountId` must be defined for using this method!") 3714 raise Exception("Account ID required") 3715 3716 else: 3717 accountId = self.accountId # use `self.accountId` (main ID) by default 3718 3719 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3720 3721 self.body = str({"accountId": accountId}) 3722 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3723 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3724 3725 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3726 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3727 rawMargin = {} 3728 3729 else: 3730 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3731 3732 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
3734 def RequestTariffLimits(self) -> dict: 3735 """ 3736 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3737 3738 See also: 3739 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3740 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3741 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3742 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3743 - `OverviewUserInfo()` method 3744 3745 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3746 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3747 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3748 """ 3749 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3750 3751 self.body = str({}) 3752 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3753 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3754 3755 uLogger.debug("Records with limits of current tariff successfully received") 3756 3757 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
3759 def RequestBondCoupons(self, iJSON: dict) -> dict: 3760 """ 3761 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3762 then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". 3763 All dates are in UTC timezone. 3764 3765 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3766 Documentation: 3767 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3768 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3769 3770 See also: `ExtendBondsData()`. 3771 3772 :param iJSON: raw json data of a bond from broker server, example: `iJSON = self.iList["Bonds"][self.ticker]` 3773 If raw iJSON is not data of bond then server returns an error [400] with message: 3774 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3775 :return: dictionary with bond payment calendar. Response example: 3776 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3777 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3778 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3779 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3780 """ 3781 if iJSON["figi"] is None or not iJSON["figi"]: 3782 uLogger.error("FIGI must be defined for using this method!") 3783 raise Exception("FIGI required") 3784 3785 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3786 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3787 3788 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3789 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3790 self.figi, 3791 startDate, 3792 endDate, 3793 )) 3794 3795 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3796 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3797 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3798 3799 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3800 uLogger.warning("Instrument type is not bond!") 3801 3802 else: 3803 uLogger.debug("Records about bond payment calendar successfully received") 3804 3805 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example:
iJSON = self.iList["Bonds"][self.ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example:
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
3807 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3808 """ 3809 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3810 pandas dataframe with more information about bonds: main info, current prices, bond payment calendar, 3811 coupon yields, current yields and some statistics etc. 3812 3813 WARNING! This is too long operation if a lot of bonds requested from broker server. 3814 3815 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3816 3817 :param instruments: list of strings with tickers or FIGIs. 3818 :param xlsx: if True then also exports pandas dataframe to xlsx-file `bondsXLSXFile`, default: `ext-bonds.xlsx`, 3819 for further used by data scientists or stock analytics. 3820 :return: wider pandas dataframe with more full and calculated data about bonds, than raw response from broker. 3821 In XLSX-file and pandas dataframe fields mean: 3822 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3823 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3824 """ 3825 if instruments is None or not instruments: 3826 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3827 raise Exception("Ticker or FIGI required") 3828 3829 if isinstance(instruments, str): 3830 instruments = [instruments] 3831 3832 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3833 3834 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3835 3836 iCount = len(uniqueInstruments) 3837 tooLong = iCount >= 20 3838 if tooLong: 3839 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3840 3841 bonds = None 3842 for i, self.figi in enumerate(uniqueInstruments): 3843 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3844 3845 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3846 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3847 rawBond = self.SearchByFIGI(requestPrice=True) 3848 3849 # Widen raw data with UTC current time (iData["actualDateTime"]): 3850 actualDate = datetime.now(tzutc()) 3851 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3852 3853 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3854 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3855 3856 # Replace some values with human-readable: 3857 iData["nominalCurrency"] = iData["nominal"]["currency"] 3858 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3859 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3860 iData["aciCurrency"] = iData["aciValue"]["currency"] 3861 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3862 iData["issueSize"] = int(iData["issueSize"]) 3863 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3864 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3865 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3866 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3867 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3868 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3869 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3870 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3871 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3872 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3873 3874 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3875 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3876 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3877 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3878 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3879 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3880 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3881 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3882 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3883 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3884 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3885 3886 # Widen raw data with calendar data from `rawCalendar` values: 3887 calendarData = [] 3888 for item in iData["rawCalendar"]["events"]: 3889 calendarData.append({ 3890 "couponDate": item["couponDate"], 3891 "couponNumber": int(item["couponNumber"]), 3892 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3893 "payCurrency": item["payOneBond"]["currency"], 3894 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3895 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3896 "couponStartDate": item["couponStartDate"], 3897 "couponEndDate": item["couponEndDate"], 3898 "couponPeriod": item["couponPeriod"], 3899 }) 3900 3901 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3902 if "maturityDate" not in iData.keys(): 3903 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3904 3905 # Widen raw data with Coupon Rate. 3906 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3907 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3908 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3909 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3910 3911 # Widen raw data with Yield to Maturity (YTM) on current date. 3912 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3913 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3914 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3915 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3916 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3917 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3918 3919 iData["calendar"] = calendarData # adds calendar at the end 3920 3921 # Remove not used data: 3922 iData.pop("uid") 3923 iData.pop("positionUid") 3924 iData.pop("currentPrice") 3925 iData.pop("rawCalendar") 3926 3927 colNames = list(iData.keys()) 3928 if bonds is None: 3929 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3930 3931 else: 3932 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3933 3934 else: 3935 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3936 3937 processed = round(100 * (i + 1) / iCount, 1) 3938 if tooLong and processed % 5 == 0: 3939 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3940 3941 else: 3942 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3943 3944 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3945 3946 # Saving bonds from pandas dataframe to XLSX sheet: 3947 if xlsx and self.bondsXLSXFile: 3948 with pd.ExcelWriter( 3949 path=self.bondsXLSXFile, 3950 date_format=TKS_DATE_FORMAT, 3951 datetime_format=TKS_DATE_TIME_FORMAT, 3952 mode="w", 3953 ) as writer: 3954 bonds.to_excel( 3955 writer, 3956 sheet_name="Extended bonds data", 3957 index=True, 3958 encoding="UTF-8", 3959 freeze_panes=(1, 1), 3960 ) # saving as XLSX-file with freeze first row and column as headers 3961 3962 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3963 3964 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider pandas dataframe with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports pandas dataframe to xlsx-file
bondsXLSXFile, default:ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider pandas dataframe with more full and calculated data about bonds, than raw response from broker. In XLSX-file and pandas dataframe fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3966 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 3967 """ 3968 Creates bond payments calendar as pandas dataframe, and also save it to the XLSX-file, `calendar.xlsx` by default. 3969 3970 WARNING! This is too long operation if a lot of bonds requested from broker server. 3971 3972 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 3973 3974 :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains 3975 extended information about bonds: main info, current prices, bond payment calendar, 3976 coupon yields, current yields and some statistics etc. 3977 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 3978 :param xlsx: if True then also exports pandas dataframe to file `calendarFile` + `".xlsx"`, default: `calendar.xlsx`, 3979 for further used by data scientists or stock analytics. 3980 :return: pandas dataframe with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 3981 """ 3982 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 3983 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 3984 3985 uLogger.debug("Generating bond payments calendar data. Wait, please...") 3986 3987 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 3988 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 3989 calendar = None 3990 for bond in extBonds.iterrows(): 3991 for item in bond[1]["calendar"]: 3992 cData = { 3993 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 3994 "couponDate": item["couponDate"], 3995 "figi": bond[1]["figi"], 3996 "ticker": bond[1]["ticker"], 3997 "name": bond[1]["name"], 3998 "couponNumber": item["couponNumber"], 3999 "payOneBond": item["payOneBond"], 4000 "payCurrency": item["payCurrency"], 4001 "couponType": item["couponType"], 4002 "couponPeriod": item["couponPeriod"], 4003 "fixDate": item["fixDate"], 4004 "couponStartDate": item["couponStartDate"], 4005 "couponEndDate": item["couponEndDate"], 4006 } 4007 4008 if calendar is None: 4009 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4010 4011 else: 4012 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4013 4014 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4015 4016 # Saving calendar from pandas dataframe to XLSX sheet: 4017 if xlsx: 4018 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4019 4020 with pd.ExcelWriter( 4021 path=xlsxCalendarFile, 4022 date_format=TKS_DATE_FORMAT, 4023 datetime_format=TKS_DATE_TIME_FORMAT, 4024 mode="w", 4025 ) as writer: 4026 humanReadable = calendar.copy(deep=True) 4027 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4028 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4029 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4030 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4031 humanReadable.columns = colNames # human-readable column names 4032 4033 humanReadable.to_excel( 4034 writer, 4035 sheet_name="Bond payments calendar", 4036 index=False, 4037 encoding="UTF-8", 4038 freeze_panes=(1, 2), 4039 ) # saving as XLSX-file with freeze first row and column as headers 4040 4041 del humanReadable # release df in memory 4042 4043 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4044 4045 return calendar
Creates bond payments calendar as pandas dataframe, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: pandas dataframe object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports pandas dataframe to file
calendarFile+".xlsx", default:calendar.xlsx, for further used by data scientists or stock analytics.
Returns
pandas dataframe with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4047 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4048 """ 4049 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4050 Also, creates Markdown file with calendar data, `calendar.md` by default. 4051 4052 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4053 4054 :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains 4055 extended information about bonds: main info, current prices, bond payment calendar, 4056 coupon yields, current yields and some statistics etc. 4057 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4058 :param show: if `True` then also printing bonds payment calendar to the console, 4059 otherwise save to file `calendarFile` only. `False` by default. 4060 :return: multilines text in Markdown format with bonds payment calendar as a table. 4061 """ 4062 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4063 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4064 4065 infoText = "# Bond payments calendar\n\n" 4066 4067 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate pandas dataframe with full calendar data 4068 4069 if not calendar.empty: 4070 splitLine = "| | | | | | | | | |\n" 4071 4072 info = [ 4073 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4074 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4075 ] 4076 4077 newMonth = False 4078 notOneBond = calendar["figi"].nunique() > 1 4079 for i, bond in enumerate(calendar.iterrows()): 4080 if newMonth and notOneBond: 4081 info.append(splitLine) 4082 4083 info.append( 4084 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4085 " +" if bond[1]["paid"] else " —", 4086 bond[1]["couponDate"].split("T")[0], 4087 bond[1]["figi"], 4088 bond[1]["ticker"], 4089 bond[1]["couponNumber"], 4090 "{} {}".format( 4091 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4092 bond[1]["payCurrency"], 4093 ), 4094 bond[1]["couponType"], 4095 bond[1]["couponPeriod"], 4096 bond[1]["fixDate"].split("T")[0], 4097 ) 4098 ) 4099 4100 if i < len(calendar.values) - 1: 4101 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4102 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4103 newMonth = False if curDate.month == nextDate.month else True 4104 4105 else: 4106 newMonth = False 4107 4108 infoText += "".join(info) 4109 4110 if show: 4111 uLogger.info("{}".format(infoText)) 4112 4113 if self.calendarFile is not None: 4114 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4115 fH.write(infoText) 4116 4117 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4118 4119 else: 4120 infoText += "No data\n" 4121 4122 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: pandas dataframe object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4124 def OverviewAccounts(self, show: bool = False) -> dict: 4125 """ 4126 Method for parsing and show simple table with all available user accounts. 4127 4128 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4129 4130 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4131 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4132 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4133 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4134 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4135 "closed": "—", "access": "Full access" }, ...}}` 4136 """ 4137 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4138 4139 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4140 accounts = { 4141 item["id"]: { 4142 "type": TKS_ACCOUNT_TYPES[item["type"]], 4143 "name": item["name"], 4144 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4145 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4146 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4147 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4148 } for item in rawAccounts["accounts"] 4149 } 4150 4151 # Raw and parsed data with some fields replaced in "stat" section: 4152 view = { 4153 "rawAccounts": rawAccounts, 4154 "stat": accounts, 4155 } 4156 4157 # --- Prepare simple text table with only accounts data in human-readable format: 4158 if show: 4159 info = [ 4160 "# User accounts\n\n", 4161 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4162 "| Account ID | Type | Status | Name |\n", 4163 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4164 ] 4165 4166 for account in view["stat"].keys(): 4167 info.extend([ 4168 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4169 account, 4170 view["stat"][account]["type"], 4171 view["stat"][account]["status"], 4172 view["stat"][account]["name"], 4173 ) 4174 ]) 4175 4176 infoText = "".join(info) 4177 4178 uLogger.info(infoText) 4179 4180 if self.userAccountsFile: 4181 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4182 fH.write(infoText) 4183 4184 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4185 4186 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4188 def OverviewUserInfo(self, show: bool = False) -> dict: 4189 """ 4190 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4191 4192 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4193 4194 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4195 :return: dict with raw parsed data from server and some calculated statistics about it. 4196 """ 4197 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4198 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4199 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4200 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4201 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4202 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4203 4204 # This is dict with parsed common user data: 4205 userInfo = { 4206 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4207 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4208 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4209 "tariff": rawUserInfo["tariff"], 4210 } 4211 4212 # This is an array of dict with parsed margin statuses for every account IDs: 4213 margins = {} 4214 for accountId in accounts.keys(): 4215 if rawMargins[accountId]: 4216 margins[accountId] = { 4217 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4218 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4219 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4220 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4221 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4222 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4223 } 4224 4225 else: 4226 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4227 4228 unary = {} # unary-connection limits 4229 for item in rawTariffLimits["unaryLimits"]: 4230 if item["limitPerMinute"] in unary.keys(): 4231 unary[item["limitPerMinute"]].extend(item["methods"]) 4232 4233 else: 4234 unary[item["limitPerMinute"]] = item["methods"] 4235 4236 stream = {} # stream-connection limits 4237 for item in rawTariffLimits["streamLimits"]: 4238 if item["limit"] in stream.keys(): 4239 stream[item["limit"]].extend(item["streams"]) 4240 4241 else: 4242 stream[item["limit"]] = item["streams"] 4243 4244 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4245 limits = { 4246 "unary": unary, 4247 "stream": stream, 4248 } 4249 4250 # Raw and parsed data as an output result: 4251 view = { 4252 "rawUserInfo": rawUserInfo, 4253 "rawAccounts": rawAccounts, 4254 "rawMargins": rawMargins, 4255 "rawTariffLimits": rawTariffLimits, 4256 "stat": { 4257 "userInfo": userInfo, 4258 "accounts": accounts, 4259 "margins": margins, 4260 "limits": limits, 4261 }, 4262 } 4263 4264 # --- Prepare text table with user information in human-readable format: 4265 if show: 4266 info = [ 4267 "# Full user information\n\n", 4268 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4269 "## Common information\n\n", 4270 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4271 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4272 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4273 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4274 "\n## User accounts\n\n", 4275 ] 4276 4277 for account in view["stat"]["accounts"].keys(): 4278 info.extend([ 4279 "### ID: [{}]\n\n".format(account), 4280 "| Parameters | Values |\n", 4281 "|----------------------|--------------------------------------------------------------|\n", 4282 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4283 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4284 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4285 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4286 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4287 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4288 ]) 4289 4290 if margins[account]: 4291 info.extend([ 4292 "| Margin status: | Enabled |\n", 4293 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4294 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4295 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4296 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4297 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4298 ]) 4299 4300 else: 4301 info.append("| Margin status: | Disabled |\n\n") 4302 4303 info.extend([ 4304 "\n## Current user tariff limits\n", 4305 "\nSee also:\n", 4306 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4307 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4308 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4309 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4310 "\n### Unary limits\n", 4311 ]) 4312 4313 if unary: 4314 for key, values in sorted(unary.items()): 4315 info.append("\n* Max requests per minute: {}\n".format(key)) 4316 4317 for value in values: 4318 info.append(" - {}\n".format(value)) 4319 4320 else: 4321 info.append("\nNot available\n") 4322 4323 info.append("\n### Stream limits\n") 4324 4325 if stream: 4326 for key, values in sorted(stream.items()): 4327 info.append("\n* Max stream connections: {}\n".format(key)) 4328 4329 for value in values: 4330 info.append(" - {}\n".format(value)) 4331 4332 else: 4333 info.append("\nNot available\n") 4334 4335 infoText = "".join(info) 4336 4337 uLogger.info(infoText) 4338 4339 if self.userInfoFile: 4340 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4341 fH.write(infoText) 4342 4343 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4344 4345 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4348class Args: 4349 """ 4350 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4351 """ 4352 def __init__(self, **kwargs): 4353 self.__dict__.update(kwargs) 4354 4355 def __getattr__(self, item): 4356 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4359def ParseArgs(): 4360 """ 4361 Function get and parse command line keys. See examples: https://tim55667757.github.io/TKSBrokerAPI/ 4362 """ 4363 parser = ArgumentParser() # command-line string parser 4364 4365 parser.description = "TKSBrokerAPI is a python API to work with some methods of Tinkoff Open API using REST protocol. It can view history, orders and market information. Also, you can open orders and trades. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples" 4366 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4367 4368 # --- options: 4369 4370 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the program. `False` by default.") 4371 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4372 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4373 4374 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4375 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4376 4377 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4378 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4379 4380 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4381 4382 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4383 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4384 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4385 4386 parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4387 4388 # --- commands: 4389 4390 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4391 4392 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4393 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4394 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider pandas dataframe with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4395 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4396 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4397 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4398 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4399 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4400 4401 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4402 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4403 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4404 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4405 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4406 4407 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4408 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4409 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4410 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4411 4412 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4413 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4414 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4415 4416 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4417 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4418 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4419 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4420 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4421 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4422 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4423 4424 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4425 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4426 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.") 4427 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.") 4428 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4429 4430 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4431 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4432 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4433 4434 cmdArgs = parser.parse_args() 4435 return cmdArgs
Function get and parse command line keys. See examples: https://tim55667757.github.io/TKSBrokerAPI/
4438def Main(**kwargs): 4439 """ 4440 Main function for work with Tinkoff Open API service. It realizes simple logic: get a lot of options and execute one command. 4441 4442 See examples: https://tim55667757.github.io/TKSBrokerAPI/ 4443 """ 4444 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4445 4446 if args.debug_level: 4447 uLogger.level = 10 # always debug level by default 4448 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4449 4450 exitCode = 0 4451 start = datetime.now(tzutc()) 4452 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4453 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4454 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4455 )) 4456 4457 # trying to calculate full current version: 4458 buildVersion = __version__ 4459 try: 4460 v = version("tksbrokerapi") 4461 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4462 4463 except Exception: 4464 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4465 4466 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4467 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4468 4469 try: 4470 if args.version: 4471 print("TKSBrokerAPI {}".format(buildVersion)) 4472 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4473 4474 else: 4475 # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader` 4476 server = TinkoffBrokerServer( 4477 token=args.token, 4478 accountId=args.account_id, 4479 useCache=not args.no_cache, 4480 ) 4481 4482 # --- set some options: 4483 4484 if args.ticker: 4485 if args.ticker in server.aliasesKeys: 4486 server.ticker = server.aliases[args.ticker] # Replace some tickers with its aliases 4487 4488 else: 4489 server.ticker = args.ticker 4490 4491 if args.figi: 4492 server.figi = args.figi 4493 4494 if args.depth is not None: 4495 server.depth = args.depth 4496 4497 # --- do one of commands: 4498 4499 if args.list: 4500 if args.output is not None: 4501 server.instrumentsFile = args.output 4502 4503 server.ShowInstrumentsInfo(show=True) 4504 4505 elif args.list_xlsx: 4506 server.DumpInstrumentsAsXLSX(forceUpdate=False) 4507 4508 elif args.bonds_xlsx is not None: 4509 if args.output is not None: 4510 server.bondsXLSXFile = args.output 4511 4512 if len(args.bonds_xlsx) == 0: 4513 server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4514 4515 else: 4516 server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4517 4518 elif args.search: 4519 if args.output is not None: 4520 server.searchResultsFile = args.output 4521 4522 server.SearchInstruments(pattern=args.search[0], show=True) 4523 4524 elif args.info: 4525 if not (args.ticker or args.figi): 4526 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4527 raise Exception("Ticker or FIGI required") 4528 4529 if args.output is not None: 4530 server.infoFile = args.output 4531 4532 if args.ticker: 4533 server.SearchByTicker(requestPrice=True, show=True, debug=False) # show info and current prices by ticker name 4534 4535 else: 4536 server.SearchByFIGI(requestPrice=True, show=True, debug=False) # show info and current prices by FIGI id 4537 4538 elif args.calendar is not None: 4539 if args.output is not None: 4540 server.calendarFile = args.output 4541 4542 if len(args.calendar) == 0: 4543 bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4544 4545 else: 4546 bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4547 4548 server.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4549 4550 elif args.price: 4551 if not (args.ticker or args.figi): 4552 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4553 raise Exception("Ticker or FIGI required") 4554 4555 server.GetCurrentPrices(show=True) 4556 4557 elif args.prices is not None: 4558 if args.output is not None: 4559 server.pricesFile = args.output 4560 4561 server.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4562 4563 elif args.overview: 4564 if args.output is not None: 4565 server.overviewFile = args.output 4566 4567 server.Overview(show=True, details="full") 4568 4569 elif args.overview_digest: 4570 if args.output is not None: 4571 server.overviewDigestFile = args.output 4572 4573 server.Overview(show=True, details="digest") 4574 4575 elif args.overview_positions: 4576 if args.output is not None: 4577 server.overviewPositionsFile = args.output 4578 4579 server.Overview(show=True, details="positions") 4580 4581 elif args.overview_orders: 4582 if args.output is not None: 4583 server.overviewOrdersFile = args.output 4584 4585 server.Overview(show=True, details="orders") 4586 4587 elif args.overview_analytics: 4588 if args.output is not None: 4589 server.overviewAnalyticsFile = args.output 4590 4591 server.Overview(show=True, details="analytics") 4592 4593 elif args.deals is not None: 4594 if args.output is not None: 4595 server.reportFile = args.output 4596 4597 if 0 <= len(args.deals) < 3: 4598 server.Deals( 4599 start=args.deals[0] if len(args.deals) >= 1 else None, 4600 end=args.deals[1] if len(args.deals) == 2 else None, 4601 show=True, # Always show deals report in console 4602 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4603 ) 4604 4605 else: 4606 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4607 raise Exception("Incorrect value") 4608 4609 elif args.history is not None: 4610 if args.output is not None: 4611 server.historyFile = args.output 4612 4613 if 0 <= len(args.history) < 3: 4614 dataReceived = server.History( 4615 start=args.history[0] if len(args.history) >= 1 else None, 4616 end=args.history[1] if len(args.history) == 2 else None, 4617 interval="hour" if args.interval is None or not args.interval else args.interval, 4618 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4619 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4620 show=True, # shows all downloaded candles in console 4621 ) 4622 4623 if args.render_chart is not None and dataReceived is not None: 4624 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4625 4626 server.ShowHistoryChart( 4627 candles=dataReceived, 4628 interact=iChart, 4629 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4630 ) 4631 4632 else: 4633 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4634 raise Exception("Incorrect value") 4635 4636 elif args.load_history is not None: 4637 histData = server.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4638 4639 if args.render_chart is not None and histData is not None: 4640 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4641 server.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4642 4643 server.ShowHistoryChart( 4644 candles=histData, 4645 interact=iChart, 4646 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4647 ) 4648 4649 elif args.trade is not None: 4650 if 1 <= len(args.trade) <= 5: 4651 server.Trade( 4652 operation=args.trade[0], 4653 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4654 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4655 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4656 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4657 ) 4658 4659 else: 4660 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4661 4662 elif args.buy is not None: 4663 if 0 <= len(args.buy) <= 4: 4664 server.Buy( 4665 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4666 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4667 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4668 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4669 ) 4670 4671 else: 4672 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4673 4674 elif args.sell is not None: 4675 if 0 <= len(args.sell) <= 4: 4676 server.Sell( 4677 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4678 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4679 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4680 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4681 ) 4682 4683 else: 4684 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4685 4686 elif args.order: 4687 if 4 <= len(args.order) <= 7: 4688 server.Order( 4689 operation=args.order[0], 4690 orderType=args.order[1], 4691 lots=int(args.order[2]), 4692 targetPrice=float(args.order[3]), 4693 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4694 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4695 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4696 ) 4697 4698 else: 4699 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4700 4701 elif args.buy_limit: 4702 server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4703 4704 elif args.sell_limit: 4705 server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4706 4707 elif args.buy_stop: 4708 if 2 <= len(args.buy_stop) <= 7: 4709 server.BuyStop( 4710 lots=int(args.buy_stop[0]), 4711 targetPrice=float(args.buy_stop[1]), 4712 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4713 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4714 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4715 ) 4716 4717 else: 4718 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4719 4720 elif args.sell_stop: 4721 if 2 <= len(args.sell_stop) <= 7: 4722 server.SellStop( 4723 lots=int(args.sell_stop[0]), 4724 targetPrice=float(args.sell_stop[1]), 4725 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4726 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4727 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4728 ) 4729 4730 else: 4731 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4732 4733 # elif args.buy_order_grid is not None: 4734 # # update order grid work with api v2 4735 # if len(args.buy_order_grid) == 2: 4736 # orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4737 # 4738 # for order in orderParams: 4739 # server.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4740 # 4741 # else: 4742 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4743 # 4744 # elif args.sell_order_grid is not None: 4745 # # update order grid work with api v2 4746 # if len(args.sell_order_grid) >= 2: 4747 # orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4748 # 4749 # for order in orderParams: 4750 # server.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4751 # 4752 # else: 4753 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4754 4755 elif args.close_order is not None: 4756 server.CloseOrders(args.close_order) # close only one order 4757 4758 elif args.close_orders is not None: 4759 server.CloseOrders(args.close_orders) # close list of orders 4760 4761 elif args.close_trade: 4762 if not args.ticker: 4763 uLogger.error("`--ticker` key is required for this operation!") 4764 raise Exception("Ticker required") 4765 4766 server.CloseTrades([args.ticker]) # close only one trade 4767 4768 elif args.close_trades is not None: 4769 server.CloseTrades(args.close_trades) # close trades for list of tickers 4770 4771 elif args.close_all is not None: 4772 server.CloseAll(*args.close_all) 4773 4774 elif args.limits: 4775 if args.output is not None: 4776 server.withdrawalLimitsFile = args.output 4777 4778 server.OverviewLimits(show=True) 4779 4780 elif args.user_info: 4781 if args.output is not None: 4782 server.userInfoFile = args.output 4783 4784 server.OverviewUserInfo(show=True) 4785 4786 elif args.account: 4787 if args.output is not None: 4788 server.userAccountsFile = args.output 4789 4790 server.OverviewAccounts(show=True) 4791 4792 else: 4793 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4794 raise Exception("There is no command to execute") 4795 4796 except Exception: 4797 trace = tb.format_exc() 4798 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4799 if e in trace: 4800 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4801 break 4802 4803 uLogger.debug(trace) 4804 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4805 exitCode = 255 # an error occurred, must be open a ticket for this issue 4806 4807 finally: 4808 finish = datetime.now(tzutc()) 4809 4810 if exitCode == 0: 4811 uLogger.debug("All operations were finished success (summary code is 0).") 4812 4813 else: 4814 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4815 os.path.abspath(uLog.defaultLogFile), exitCode, 4816 )) 4817 4818 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4819 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4820 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4821 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4822 )) 4823 4824 if not kwargs: 4825 sys.exit(exitCode) 4826 4827 else: 4828 return exitCode
Main function for work with Tinkoff Open API service. It realizes simple logic: get a lot of options and execute one command.
See examples: https://tim55667757.github.io/TKSBrokerAPI/